3
0

Simple Example Agent

This commit is contained in:
Magnus Bender 2024-10-30 23:47:34 +01:00
parent ebe4a58f90
commit 7199cfc714
Signed by: bender
GPG Key ID: 5149A211831F2BD7
7 changed files with 261 additions and 20 deletions

View File

@ -1,3 +1,86 @@
# 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 [&darr;](#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
View File

@ -0,0 +1,3 @@
hello hello hello
x = 10
bye bye

2
data/share/example/y.txt Normal file
View File

@ -0,0 +1,2 @@
y = 15
huhu huhu huhu

2
data/share/example/z.txt Normal file
View File

@ -0,0 +1,2 @@
z = 20
blubb

View File

@ -8,19 +8,33 @@
# 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)
@ -28,12 +42,37 @@ class SimpleExtractImageAgent(ExtractImageAgent):
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 = [

View File

@ -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 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, riddle: Riddle, data: RiddleData) -> RiddleSolution:
# remove whitespace
expression = riddle.question.strip()
if self.message().id == "test":
status = RiddleStatus()
status.extract.required = False
self.sub_riddle(riddle=Riddle(context="Haha", question="Blubber"), status=status)
# 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
return RiddleSolution(solution="Huii", explanation="Blubb")
# 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 = [

View File

@ -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