Agent should work

This commit is contained in:
2024-10-29 23:57:04 +01:00
parent 17d96cd069
commit 533b9fed6d
24 changed files with 703 additions and 19 deletions

View File

@ -6,4 +6,13 @@
# https://www.chai.uni-hamburg.de/~bender
#
# 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
from ums.agent.agent import (
AgentCapability,
BasicAgent,
ExtractAgent,
ExtractAudioAgent, ExtractImageAgent, ExtractTextAgent,
SolveAgent,
GatekeeperAgent
)

243
ums/agent/agent.py Normal file
View File

@ -0,0 +1,243 @@
import random
from abc import abstractmethod, ABC
from enum import Enum
from typing import List, Callable
from ums.utils import (
RiddleInformation, AgentMessage, RiddleDataType, RiddleData, Riddle,
RiddleStatus, RiddleSolution,
logger
)
class AgentCapability(Enum):
"""
The three different capabilities an agent can have.
"""
EXTRACT="extract"
SOLVE="solve"
GATEKEEPER="gatekeeper"
class BasicAgent(ABC):
"""
A basic agent, each agent will be a subclass of this class.
"""
@staticmethod
@abstractmethod
def agent_capability() -> AgentCapability:
"""
Represents the capabilities of this agent, for messages/ tasks of this capability, the `handle` method will be called.
"""
pass
def __init__(self, message:AgentMessage, send_message:Callable[[AgentMessage], bool]):
self._send_message = send_message
self._sub_cnt = 0
self._message = message
self._response = message.model_copy(deep=True)
self._do_response = False
self._process()
self._respond()
@abstractmethod
def _process(self):
pass
def _respond(self):
send_it = lambda: self._send_message(self._response)
if self.before_response(self._response, send_it) and self._do_response:
send_it()
logger.debug(f"Response sent {self._response.id}")
else:
logger.debug(f"Stopped response {self._response.id}")
def before_response(self, response:AgentMessage, send_it:Callable[[], None]) -> bool:
"""
This method is called before the response is sent.
If the method returns `False` no response will be sent.
Thus, by overwriting this method, a response can be prevented.
The response to be sent is in `response` and `send_it` is a callable, which sends the response to the management if it gets called.
(Hence, one may stop sending the response and later call `send_it()` to send the response.)
"""
return True
def message(self) -> AgentMessage:
"""
Get the message this agent object is working on.
"""
return self._message;
def sub_riddle(self,
riddle:Riddle, data:List[RiddleData]=[], status:RiddleStatus=None
) -> AgentMessage|bool:
"""
Create a new sub-riddle for solving details of the current riddle.
For the sub-riddle, give a `riddle` and optionally, a selection of `data` items (default none) and a `status` (default `ums.utils.types.RiddleStatus()`).
By changing a status, different steps can be (de-)selected.
Return the message of the sub-riddle or `false` on error.
"""
if status is None:
status = RiddleStatus()
# new sub riddle id
self._sub_cnt += 1
new_id = "{}-sub-{}.{}".format(self._message.id, self._sub_cnt, int(random.random()*100))
self._message.sub_ids.append(new_id)
self._response.sub_ids.append(new_id)
# create the riddle's message
sub_msg = AgentMessage(
id=new_id,
riddle=riddle,
data=data,
status=status
)
logger.debug(f"Created sub-riddle {sub_msg.id}")
# send it
if self._send_message(sub_msg):
return sub_msg
else:
return False
@abstractmethod
def handle(self, *args:RiddleInformation) -> RiddleInformation:
"""
Handle a single task of the agent, the arguments and return value depends on the actual task (see subclass)!
**This is the method to implement!**
The full message is available via `message()`, a sub riddle can be created with `sub_riddle()`.
"""
pass
class ExtractAgent(BasicAgent):
"""
An extraction agent.
"""
def agent_capability() -> AgentCapability:
return AgentCapability.EXTRACT
@staticmethod
@abstractmethod
def extract_type() -> RiddleDataType:
"""
Represents the data this agent can process.
"""
pass
def _process(self):
for i, data in enumerate(self._response.data):
if data.type == self.__class__.extract_type():
logger.debug(f"Start extraction '{data.file_plain}'")
result = self.handle(data)
logger.debug(f"End extraction '{data.file_plain}' ('{result.file_extracted}')")
if result.file_extracted is None:
logger.info(f"Riddle {self._response.id}: 'file_extracted' for data '{data.file_plain}' still empty after handling")
self._response.data[i] = result
self._do_response = True
self._response.status.extract.finished = True
@abstractmethod
def handle(self, data:RiddleData) -> RiddleData:
"""
Process the item `data`, create extraction file and return `data` with populated `data.file_extracted`.
"""
class ExtractTextAgent(ExtractAgent):
"""
An extraction agent for text, create a subclass for your agent.
"""
def extract_type() -> RiddleDataType:
return RiddleDataType.TEXT
class ExtractAudioAgent(ExtractAgent):
"""
An extraction agent for audio, create a subclass for your agent.
"""
def extract_type() -> RiddleDataType:
return RiddleDataType.AUDIO
class ExtractImageAgent(ExtractAgent):
"""
An extraction agent for images, create a subclass for your agent.
"""
def extract_type() -> RiddleDataType:
return RiddleDataType.IMAGE
class SolveAgent(BasicAgent):
"""
A solve agent, create a subclass for your agent.
"""
def agent_capability() -> AgentCapability:
return AgentCapability.SOLVE
def _process(self):
logger.debug(f"Start solve: {self._response.id}")
solution = self.handle(self._response.riddle, self._response.data)
logger.debug(f"End solve: {self._response.id} ({solution.solution}, {solution.explanation})")
if len(solution.solution) == 0 or len(solution.explanation) == 0:
logger.info(f"Riddle {self._response.id}: Empty solution/ explanation after handling")
self._response.solution = solution
self._response.status.solve.finished = True
self._do_response = True
@abstractmethod
def handle(self, riddle:Riddle, data:RiddleData) -> RiddleSolution:
"""
Solve the `riddle` using `data` and return a solution.
"""
class GatekeeperAgent(BasicAgent):
"""
A gatekeeper agent, create a subclass for your agent.
"""
def agent_capability() -> AgentCapability:
return AgentCapability.GATEKEEPER
def _process(self):
if self._response.solution is None:
self._response.solution = RiddleSolution(solution="", explanation="")
logger.debug(f"Start validate: {self._response.id}")
solution = self.handle(self._response.solution, self._response.riddle)
logger.debug(f"End validate: {self._response.id} ({solution.review}, {solution.accepted})")
if solution.review is None or len(solution.review) == 0:
logger.info(f"Riddle {self._response.id}: Empty review after handling")
self._response.solution = solution
self._response.status.validate.finished = True
self._response.status.solved = solution.accepted
self._do_response = True
@abstractmethod
def handle(self, solution:RiddleSolution, riddle:Riddle) -> RiddleSolution:
"""
Check the `solution` of `riddle` and return solution with populated `solution.accepted` and `solution.review`.
"""

65
ums/agent/main.py Normal file
View File

@ -0,0 +1,65 @@
# Agenten Plattform
#
# (c) 2024 Magnus Bender
# Institute of Humanities-Centered Artificial Intelligence (CHAI)
# Universitaet Hamburg
# https://www.chai.uni-hamburg.de/~bender
#
# source code released under the terms of GNU Public License Version 3
# https://www.gnu.org/licenses/gpl-3.0.txt
import os
from fastapi import FastAPI, Request, BackgroundTasks
from fastapi.staticfiles import StaticFiles
from fastapi.responses import JSONResponse
from ums.agent.process import MessageProcessor
from ums.utils import AgentMessage, AgentResponse, const
class WebMain():
def __init__(self):
self.msg_process = MessageProcessor()
self._init_app()
self._add_routes()
def _init_app(self):
self.app = FastAPI(
title="Agenten Plattform",
description="Agenten Plattform Agent",
openapi_url="/api/schema.json",
docs_url='/api',
redoc_url=None
)
self.app.mount(
"/static",
StaticFiles(directory=os.path.join(const.PUBLIC_PATH, 'static')),
name="static"
)
self.app.mount(
"/docs",
StaticFiles(directory=os.path.join(const.PUBLIC_PATH, 'docs'), html=True),
name="docs"
)
def _add_routes(self):
@self.app.get("/", response_class=JSONResponse, summary="Link list")
def index():
return {
"title" : "Agenten Plattform Agent",
"./message" : "Messaged from the Management",
"./api" : "API Overview",
"./docs" : "Documentation"
}
@self.app.post("/message", summary="Send a message to this agent")
def message(request: Request, message:AgentMessage, background_tasks: BackgroundTasks) -> AgentResponse:
return self.msg_process.new_message(message, background_tasks)
if __name__ == "ums.agent.main" and os.environ.get('SERVE', 'false') == 'true':
main = WebMain()
app = main.app

95
ums/agent/process.py Normal file
View File

@ -0,0 +1,95 @@
# Agenten Plattform
#
# (c) 2024 Magnus Bender
# Institute of Humanities-Centered Artificial Intelligence (CHAI)
# Universitaet Hamburg
# https://www.chai.uni-hamburg.de/~bender
#
# source code released under the terms of GNU Public License Version 3
# https://www.gnu.org/licenses/gpl-3.0.txt
import os, importlib
from typing import List
import requests
from fastapi import BackgroundTasks
from ums.agent.agent import BasicAgent, AgentCapability, ExtractAgent, SolveAgent, GatekeeperAgent
from ums.utils import AgentMessage, AgentResponse, logger
class MessageProcessor():
MANAGEMENT_URL = os.environ.get('MANAGEMENT_URL', 'http://127.0.0.1:80').strip().strip('/')
AGENTS_LIST = os.environ.get('AGENTS_LIST', 'ums.example.example:AGENT_CLASSES').strip()
def __init__(self):
self.counts = 0
module_name, var_name = self.AGENTS_LIST.split(':')
agents_module = importlib.import_module(module_name)
self.agent_classes:List[BasicAgent] = getattr(agents_module, var_name)
self.extract_agents:List[ExtractAgent] = list(filter(
lambda ac: ac.agent_capability() == AgentCapability.EXTRACT,
self.agent_classes
))
self.solve_agents:List[SolveAgent] = list(filter(
lambda ac: ac.agent_capability() == AgentCapability.SOLVE,
self.agent_classes
))
self.gatekeeper_agents:List[GatekeeperAgent] = list(filter(
lambda ac: ac.agent_capability() == AgentCapability.GATEKEEPER,
self.agent_classes
))
def new_message(self, message:AgentMessage, background_tasks: BackgroundTasks) -> AgentResponse:
enqueued = False
if message.status.extract.required and not message.status.extract.finished:
# send to extract agents
if len(self.extract_agents) > 0:
data_types = set( d.type for d in message.data )
for ac in self.extract_agents:
if ac.extract_type() in data_types:
background_tasks.add_task(ac, message, self._send_message)
enqueued = True
elif message.status.solve.required and not message.status.solve.finished:
# send to solve agents
if len(self.solve_agents) > 0:
for sa in self.solve_agents:
background_tasks.add_task(sa, message, self._send_message)
enqueued = True
elif message.status.validate.required and not message.status.validate.finished:
# send to solve agents
if len(self.gatekeeper_agents) > 0:
for ga in self.gatekeeper_agents:
background_tasks.add_task(ga, message, self._send_message)
enqueued = True
logger.debug(
("Added to queue" if enqueued else "No agent found to queue message.") +
f"ID: {message.id} Count: {self.counts}"
)
self.counts += 1
return AgentResponse(
count=self.counts-1,
msg="Added to queue" if enqueued else "",
error=not enqueued,
error_msg=None if enqueued else "No agent found to queue message."
)
def _send_message(self, message:AgentMessage) -> bool:
r = requests.post(
"{}/message".format(self.MANAGEMENT_URL),
data=message.model_dump_json(),
headers={"accept" : "application/json", "content-type" : "application/json"}
)
if r.status_code == 200:
return True
else:
logger.warning(f"Error sending message to management! {(r.text, r.headers)}")
return False