Simple Example Agent
This commit is contained in:
parent
ebe4a58f90
commit
7199cfc714
85
Example.md
85
Example.md
@ -1,3 +1,86 @@
|
|||||||
# Beispiel: Rätsel & Agent
|
# Beispiel: Rätsel & Agent
|
||||||
|
|
||||||
TODO
|
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 <http://localhost:8080/app/new> 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 <http://localhost:8080/app/table> 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 (<http://localhost:8080/docs/ums/agent/agent.html#BasicAgent.sub_riddle>)
|
||||||
|
- 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 (<http://localhost:8080/docs/ums/agent/agent.html#BasicAgent.before_response>)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
```
|
3
data/share/example/x.txt
Normal file
3
data/share/example/x.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
hello hello hello
|
||||||
|
x = 10
|
||||||
|
bye bye
|
2
data/share/example/y.txt
Normal file
2
data/share/example/y.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
y = 15
|
||||||
|
huhu huhu huhu
|
2
data/share/example/z.txt
Normal file
2
data/share/example/z.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
z = 20
|
||||||
|
blubb
|
@ -8,32 +8,71 @@
|
|||||||
# source code released under the terms of GNU Public License Version 3
|
# source code released under the terms of GNU Public License Version 3
|
||||||
# https://www.gnu.org/licenses/gpl-3.0.txt
|
# https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from ums.agent import ExtractAudioAgent, ExtractImageAgent, ExtractTextAgent
|
from ums.agent import ExtractAudioAgent, ExtractImageAgent, ExtractTextAgent
|
||||||
from ums.utils.types import RiddleData, AgentMessage
|
from ums.utils.types import RiddleData, AgentMessage
|
||||||
|
from ums.utils.schema import ExtractedData, ExtractedContent, ExtractedPositions
|
||||||
|
|
||||||
class SimpleExtractAudioAgent(ExtractAudioAgent):
|
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:
|
def handle(self, data: RiddleData) -> RiddleData:
|
||||||
print("Audio Process:", data.file_plain)
|
print("Audio Process:", data.file_plain)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class SimpleExtractImageAgent(ExtractImageAgent):
|
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:
|
def handle(self, data: RiddleData) -> RiddleData:
|
||||||
print("Image Process:", data.file_plain)
|
print("Image Process:", data.file_plain)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class SimpleExtractTextAgent(ExtractTextAgent):
|
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:
|
def handle(self, data: RiddleData) -> RiddleData:
|
||||||
print("Text Process:", data.file_plain)
|
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
|
return data
|
||||||
|
|
||||||
AGENT_CLASSES = [
|
AGENT_CLASSES = [
|
||||||
|
@ -9,20 +9,91 @@
|
|||||||
# https://www.gnu.org/licenses/gpl-3.0.txt
|
# 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):
|
class SimpleSolveAgent(SolveAgent):
|
||||||
|
|
||||||
def handle(self, riddle: Riddle, data: RiddleData) -> RiddleSolution:
|
def before_response(self, response: AgentMessage, send_it: Callable[[], None]) -> bool:
|
||||||
|
# do not send a response, if this is not a calculator riddle!
|
||||||
if self.message().id == "test":
|
return not self.stop_response
|
||||||
status = RiddleStatus()
|
|
||||||
status.extract.required = False
|
|
||||||
self.sub_riddle(riddle=Riddle(context="Haha", question="Blubber"), status=status)
|
|
||||||
|
|
||||||
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 = [
|
AGENT_CLASSES = [
|
||||||
|
@ -8,17 +8,58 @@
|
|||||||
# source code released under the terms of GNU Public License Version 3
|
# source code released under the terms of GNU Public License Version 3
|
||||||
# https://www.gnu.org/licenses/gpl-3.0.txt
|
# https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
from ums.agent import GatekeeperAgent
|
from ums.agent import GatekeeperAgent
|
||||||
|
from ums.utils.types import Riddle, RiddleSolution, AgentMessage
|
||||||
from ums.utils.types import Riddle, RiddleSolution
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleSolveAgent(GatekeeperAgent):
|
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:
|
def handle(self, solution: RiddleSolution, riddle: Riddle) -> RiddleSolution:
|
||||||
solution.accepted = True
|
self.stop_response = False
|
||||||
solution.review = "Ok"
|
|
||||||
|
# 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
|
return solution
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user