Agent should work
This commit is contained in:
@ -24,8 +24,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Build the Management
|
- name: Build the Management
|
||||||
run: bash ./build-mgmt.sh -no-updates
|
run: bash ./build-mgmt.sh -no-updates
|
||||||
#- name: Build the Agent
|
- name: Build the Agent
|
||||||
# run: bash ./build-agent.sh -no-updates
|
run: bash ./build-agent.sh -no-updates
|
||||||
|
|
||||||
- name: Docker login
|
- name: Docker login
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
- `./docker-agent/`
|
- `./docker-agent/`
|
||||||
- `./ums/agent/`
|
- `./ums/agent/`
|
||||||
|
- `./build-agent.sh`
|
||||||
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
50
build-agent.sh
Executable file
50
build-agent.sh
Executable file
@ -0,0 +1,50 @@
|
|||||||
|
#/bin/bash
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# https://stackoverflow.com/a/4774063
|
||||||
|
SCRIPTPATH="$(cd -- "$(dirname "$0")" >/dev/null 2>&1; pwd -P)"
|
||||||
|
|
||||||
|
source "$SCRIPTPATH/vars.sh"
|
||||||
|
|
||||||
|
requirements="requirements-frozen.txt"
|
||||||
|
if [ "$1" != "-no-updates" ]; then
|
||||||
|
echo "Update the depencendies in requirements.txt? [may break app] (y/n)"
|
||||||
|
read newlockfile
|
||||||
|
if [ "$newlockfile" == "y" ]; then
|
||||||
|
requirements="requirements.txt"
|
||||||
|
fi;
|
||||||
|
fi;
|
||||||
|
|
||||||
|
for platform in $PLATFORMS; do
|
||||||
|
if [ "$platform" == "gpu" ]; then
|
||||||
|
platform="amd64"
|
||||||
|
tag="gpu-amd64"
|
||||||
|
else
|
||||||
|
tag="cpu-$platform"
|
||||||
|
fi;
|
||||||
|
|
||||||
|
docker build \
|
||||||
|
--pull \
|
||||||
|
--platform "linux/$platform" \
|
||||||
|
--file "$SCRIPTPATH/docker-agent/Dockerfile" \
|
||||||
|
--build-arg FROM_IMAGE="$IMAGE_REGISTRY/$IMAGE_OWNER/$IMAGE_AGENT_BASE:$tag" \
|
||||||
|
--build-arg PIP_REQ_FILE="$requirements" \
|
||||||
|
--tag "$IMAGE_REGISTRY/$IMAGE_OWNER/$IMAGE_NAME_AGENT:$tag" \
|
||||||
|
"$SCRIPTPATH"
|
||||||
|
done;
|
||||||
|
|
||||||
|
if [ "$requirements" == "requirements.txt" ]; then
|
||||||
|
# extract requirements-frozen.txt
|
||||||
|
cid=$(docker create "$IMAGE_REGISTRY/$IMAGE_OWNER/$IMAGE_NAME_AGENT:cpu-arm64")
|
||||||
|
docker cp "$cid:/ums-agenten/requirements-frozen.txt" "$SCRIPTPATH/docker-agent/requirements-frozen.txt"
|
||||||
|
docker rm "$cid"
|
||||||
|
fi;
|
@ -8,8 +8,28 @@
|
|||||||
# 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
|
||||||
|
|
||||||
ARG FROM_IMAGE=
|
ARG FROM_IMAGE
|
||||||
|
|
||||||
FROM $FROM_IMAGE
|
FROM $FROM_IMAGE
|
||||||
|
|
||||||
|
ARG PIP_REQ_FILE
|
||||||
|
|
||||||
|
USER root
|
||||||
|
RUN mkdir -p /ums-agenten/plattform/ && mkdir -p /ums-agenten/persist/
|
||||||
|
|
||||||
|
COPY ./docker-agent/$PIP_REQ_FILE /ums-agenten/requirements.txt
|
||||||
|
RUN pip3 install --break-system-packages --no-cache-dir -r /ums-agenten/requirements.txt \
|
||||||
|
&& pip3 freeze -q -r /ums-agenten/requirements.txt > /ums-agenten/requirements-frozen.txt
|
||||||
|
|
||||||
|
# install the code of the repo
|
||||||
|
COPY ./docker-mgmt/setup.py /ums-agenten/plattform/
|
||||||
|
RUN pip3 install --break-system-packages -e /ums-agenten/plattform/
|
||||||
|
|
||||||
|
COPY --chown=user:user ./ums/ /ums-agenten/plattform/ums/
|
||||||
|
COPY --chown=user:user ./web/ /ums-agenten/plattform/web/
|
||||||
|
|
||||||
|
WORKDIR /ums-agenten/plattform/ums/
|
||||||
|
RUN chown -R user:user /ums-agenten
|
||||||
|
USER user
|
||||||
|
|
||||||
|
ENV SERVE=true
|
||||||
|
CMD ["/usr/local/bin/uvicorn", "ums.agent.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
16
docker-agent/requirements-frozen.txt
Normal file
16
docker-agent/requirements-frozen.txt
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# non frozen dependecies, to use latest
|
||||||
|
requests==2.32.3
|
||||||
|
fastapi==0.115.4
|
||||||
|
uvicorn==0.32.0
|
||||||
|
python-multipart==0.0.16
|
||||||
|
## The following requirements were added by pip freeze:
|
||||||
|
# ...
|
15
docker-agent/requirements.txt
Normal file
15
docker-agent/requirements.txt
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# non frozen dependecies, to use latest
|
||||||
|
requests
|
||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
python-multipart
|
@ -20,10 +20,10 @@ services:
|
|||||||
- 8000:80
|
- 8000:80
|
||||||
environment:
|
environment:
|
||||||
- SOLUTION_MAX_TRIALS=5
|
- SOLUTION_MAX_TRIALS=5
|
||||||
- MANAGEMENT_URL=http://management:8000
|
- MANAGEMENT_URL=http://management
|
||||||
- AGENTS_PROCESS=http://agent_process_1:3001,http://agent_process_2:3001
|
- AGENTS_PROCESS=http://agent_all:8000
|
||||||
- AGENTS_SOLVE=http://agent_solve_1:3001
|
- AGENTS_SOLVE=http://agent_all:8000
|
||||||
- AGENTS_GATEKEEPER=http://agent_gatekeeper_1:3001
|
- AGENTS_GATEKEEPER=http://agent_all:8000
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/share/:/ums-agenten/share/
|
- ./data/share/:/ums-agenten/share/
|
||||||
- ./data/persist-management/:/ums-agenten/persist/
|
- ./data/persist-management/:/ums-agenten/persist/
|
||||||
@ -33,3 +33,21 @@ services:
|
|||||||
# enable auto reloading (for development)
|
# enable auto reloading (for development)
|
||||||
entrypoint: bash -c "nginx; SERVE=true uvicorn ums.management.main:app --uds /tmp/uvicorn.sock --proxy-headers --reload"
|
entrypoint: bash -c "nginx; SERVE=true uvicorn ums.management.main:app --uds /tmp/uvicorn.sock --proxy-headers --reload"
|
||||||
|
|
||||||
|
agent_all:
|
||||||
|
image: git.chai.uni-hamburg.de/ums-agenten/base-agent:cpu-arm64
|
||||||
|
#image: git.chai.uni-hamburg.de/ums-agenten/base-agent:cpu-amd64
|
||||||
|
#image: git.chai.uni-hamburg.de/ums-agenten/base-agent:gpu-amd64
|
||||||
|
ports:
|
||||||
|
- 8001:8000
|
||||||
|
environment:
|
||||||
|
- AGENTS_LIST=ums.example.example:AGENT_CLASSES
|
||||||
|
- MANAGEMENT_URL=http://management
|
||||||
|
volumes:
|
||||||
|
- ./data/share/:/ums-agenten/share/
|
||||||
|
- ./data/persist-all/:/ums-agenten/persist/
|
||||||
|
# bind code from host to container (for development)
|
||||||
|
- ./ums/:/ums-agenten/plattform/ums/:ro
|
||||||
|
- ./web/:/ums-agenten/plattform/web/
|
||||||
|
# enable auto reloading (for development)
|
||||||
|
entrypoint: bash -c "SERVE=true uvicorn ums.agent.main:app --host 0.0.0.0 --port 8000 --reload"
|
||||||
|
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
[supervisord]
|
[supervisord]
|
||||||
nodaemon=true
|
nodaemon=true
|
||||||
user=root
|
user=root
|
||||||
|
logfile=/dev/stdout
|
||||||
|
logfile_maxbytes = 0
|
||||||
|
|
||||||
[program:setup]
|
[program:setup]
|
||||||
command=/bin/sh -c "chown user:user -R /ums-agenten"
|
command=/bin/sh -c "chown user:user -R /ums-agenten"
|
||||||
|
@ -7,3 +7,12 @@
|
|||||||
#
|
#
|
||||||
# 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
|
||||||
|
|
||||||
|
from ums.agent.agent import (
|
||||||
|
AgentCapability,
|
||||||
|
BasicAgent,
|
||||||
|
ExtractAgent,
|
||||||
|
ExtractAudioAgent, ExtractImageAgent, ExtractTextAgent,
|
||||||
|
SolveAgent,
|
||||||
|
GatekeeperAgent
|
||||||
|
)
|
243
ums/agent/agent.py
Normal file
243
ums/agent/agent.py
Normal 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
65
ums/agent/main.py
Normal 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
95
ums/agent/process.py
Normal 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
|
9
ums/example/__init__.py
Normal file
9
ums/example/__init__.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# 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
|
@ -11,6 +11,8 @@
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
## Example: Sending messages to management via python
|
||||||
|
|
||||||
from ums.utils import AgentMessage, RiddleData, RiddleDataType, RiddleSolution, ManagementRequest
|
from ums.utils import AgentMessage, RiddleData, RiddleDataType, RiddleSolution, ManagementRequest
|
||||||
|
|
||||||
ex = AgentMessage(
|
ex = AgentMessage(
|
71
ums/example/example.py
Normal file
71
ums/example/example.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
from typing import Callable
|
||||||
|
from ums.agent import ExtractAudioAgent, ExtractImageAgent, ExtractTextAgent, SolveAgent, GatekeeperAgent
|
||||||
|
|
||||||
|
from ums.utils.types import AgentMessage, Riddle, RiddleData, RiddleSolution, RiddleStatus
|
||||||
|
|
||||||
|
"""
|
||||||
|
Examples for simple agents.
|
||||||
|
|
||||||
|
Each agent is represented by its own class. The handling of tasks is done by `handle()` in each agent.
|
||||||
|
|
||||||
|
Finally `AGENT_CLASSES` contains the classes of the agents in a list. Via environmental variables this list is specified to the ums.agent system.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class MyExtractAudioAgent(ExtractAudioAgent):
|
||||||
|
|
||||||
|
def handle(self, data: RiddleData) -> RiddleData:
|
||||||
|
print("Audio Process:", data.file_plain)
|
||||||
|
return data
|
||||||
|
|
||||||
|
class MyExtractImageAgent(ExtractImageAgent):
|
||||||
|
|
||||||
|
def handle(self, data: RiddleData) -> RiddleData:
|
||||||
|
print("Image Process:", data.file_plain)
|
||||||
|
return data
|
||||||
|
|
||||||
|
class MyExtractTextAgent(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)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class MySolveAgent(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)
|
||||||
|
|
||||||
|
return RiddleSolution(solution="Huii", explanation="Blubb")
|
||||||
|
|
||||||
|
|
||||||
|
class MyGatekeeperAgent(GatekeeperAgent):
|
||||||
|
|
||||||
|
def handle(self, solution: RiddleSolution, riddle: Riddle) -> RiddleSolution:
|
||||||
|
solution.accepted = True
|
||||||
|
solution.review = "Ok"
|
||||||
|
|
||||||
|
return solution
|
||||||
|
|
||||||
|
AGENT_CLASSES = [
|
||||||
|
MyExtractAudioAgent, MyExtractImageAgent, MyExtractTextAgent,
|
||||||
|
MySolveAgent,
|
||||||
|
MyGatekeeperAgent
|
||||||
|
]
|
@ -15,7 +15,7 @@ import requests
|
|||||||
from fastapi import BackgroundTasks
|
from fastapi import BackgroundTasks
|
||||||
|
|
||||||
from ums.management.db import DB
|
from ums.management.db import DB
|
||||||
from ums.utils import AgentMessage, AgentResponse
|
from ums.utils import AgentMessage, AgentResponse, logger
|
||||||
|
|
||||||
class MessageProcessor():
|
class MessageProcessor():
|
||||||
|
|
||||||
@ -41,11 +41,11 @@ class MessageProcessor():
|
|||||||
self.management_name = self._get_name(self.MANAGEMENT_URL)
|
self.management_name = self._get_name(self.MANAGEMENT_URL)
|
||||||
|
|
||||||
if len(self.AGENTS_PROCESS) == 0:
|
if len(self.AGENTS_PROCESS) == 0:
|
||||||
print("Not Process Agent (AGENTS_PROCESS) found, this may be a problem!")
|
logger.warning(f"Not Process Agent (AGENTS_PROCESS) found, this may be a problem!")
|
||||||
if len(self.AGENTS_SOLVE) == 0:
|
if len(self.AGENTS_SOLVE) == 0:
|
||||||
print("Not Solve Agent (AGENTS_SOLVE) found, this may be a problem!")
|
logger.warning(f"Not Solve Agent (AGENTS_SOLVE) found, this may be a problem!")
|
||||||
if len(self.AGENTS_GATEKEEPER) == 0:
|
if len(self.AGENTS_GATEKEEPER) == 0:
|
||||||
print("Not Gatekeeper Agent (AGENTS_GATEKEEPER) found, this may be a problem!")
|
logger.warning(f"Not Gatekeeper Agent (AGENTS_GATEKEEPER) found, this may be a problem!")
|
||||||
|
|
||||||
def _get_name(self, url:str) -> str:
|
def _get_name(self, url:str) -> str:
|
||||||
m = re.match(r'^https?://([^:]*)(?::(\d+))?$', url)
|
m = re.match(r'^https?://([^:]*)(?::(\d+))?$', url)
|
||||||
@ -92,6 +92,11 @@ class MessageProcessor():
|
|||||||
self._send_messages(self.AGENTS_GATEKEEPER, db_message.message)
|
self._send_messages(self.AGENTS_GATEKEEPER, db_message.message)
|
||||||
|
|
||||||
else: # all steps "done"
|
else: # all steps "done"
|
||||||
|
|
||||||
|
# validate not required? (then solved will never be set to true, thus set it here)
|
||||||
|
if not db_message.message.status.validate.required:
|
||||||
|
db_message.message.status.solved = True
|
||||||
|
|
||||||
if db_message.message.status.solved:
|
if db_message.message.status.solved:
|
||||||
# yay, message is solved
|
# yay, message is solved
|
||||||
self.db.set_solution(count=count, solution=True);
|
self.db.set_solution(count=count, solution=True);
|
||||||
@ -155,6 +160,6 @@ class MessageProcessor():
|
|||||||
self.db.set_processed(db_count, processed=True)
|
self.db.set_processed(db_count, processed=True)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print("Error sending message to:", recipient, (r.text, r.headers))
|
logger.warning(f"Error sending message to: {recipient} {(r.text, r.headers)}")
|
||||||
return False
|
return False
|
||||||
|
|
@ -8,6 +8,21 @@
|
|||||||
# 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
|
||||||
|
|
||||||
|
from ums.utils.const import *
|
||||||
|
|
||||||
|
import logging, os
|
||||||
|
if os.environ.get('SERVE', 'false') == 'true':
|
||||||
|
logging.basicConfig(
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler(LOG_FILE),
|
||||||
|
logging.StreamHandler()
|
||||||
|
],
|
||||||
|
level=LOG_LEVEL,
|
||||||
|
format='%(asctime)s %(levelname)s %(name)s: %(message)s',
|
||||||
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger('UMS Agenten')
|
||||||
|
|
||||||
from ums.utils.types import (
|
from ums.utils.types import (
|
||||||
RiddleInformation,
|
RiddleInformation,
|
||||||
AgentMessage,
|
AgentMessage,
|
||||||
@ -20,8 +35,6 @@ from ums.utils.types import (
|
|||||||
MessageDbRow
|
MessageDbRow
|
||||||
)
|
)
|
||||||
|
|
||||||
from ums.utils.const import *
|
|
||||||
|
|
||||||
from ums.utils.request import ManagementRequest
|
from ums.utils.request import ManagementRequest
|
||||||
|
|
||||||
from ums.utils.functions import list_shared_data, list_shared_schema
|
from ums.utils.functions import list_shared_data, list_shared_schema
|
@ -13,9 +13,13 @@
|
|||||||
See the content ...
|
See the content ...
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os, logging
|
||||||
|
|
||||||
BASE_PATH = '/ums-agenten'
|
BASE_PATH = '/ums-agenten'
|
||||||
SHARE_PATH = os.path.join(BASE_PATH, 'share')
|
SHARE_PATH = os.path.join(BASE_PATH, 'share')
|
||||||
PERSIST_PATH = os.path.join(BASE_PATH, 'persist')
|
PERSIST_PATH = os.path.join(BASE_PATH, 'persist')
|
||||||
|
PUBLIC_PATH = os.path.join(BASE_PATH, 'plattform', 'web', 'public')
|
||||||
TEMPLATE_PATH = os.path.join(BASE_PATH, 'plattform', 'web', 'templates')
|
TEMPLATE_PATH = os.path.join(BASE_PATH, 'plattform', 'web', 'templates')
|
||||||
|
|
||||||
|
LOG_FILE = os.path.join(PERSIST_PATH, 'ums.log')
|
||||||
|
LOG_LEVEL = logging.INFO
|
@ -1,3 +1,13 @@
|
|||||||
|
# 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
|
import os
|
||||||
|
|
||||||
from typing import List, Callable
|
from typing import List, Callable
|
||||||
|
1
vars.sh
1
vars.sh
@ -13,5 +13,6 @@
|
|||||||
IMAGE_REGISTRY="git.chai.uni-hamburg.de"
|
IMAGE_REGISTRY="git.chai.uni-hamburg.de"
|
||||||
IMAGE_OWNER="ums-agenten"
|
IMAGE_OWNER="ums-agenten"
|
||||||
IMAGE_NAME_AGENT="base-agent"
|
IMAGE_NAME_AGENT="base-agent"
|
||||||
|
IMAGE_AGENT_BASE="base-image"
|
||||||
IMAGE_NAME_MGMT="management"
|
IMAGE_NAME_MGMT="management"
|
||||||
PLATFORMS="amd64 arm64 gpu"
|
PLATFORMS="amd64 arm64 gpu"
|
||||||
|
@ -1,3 +1,11 @@
|
|||||||
|
/** 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
|
||||||
|
**/
|
||||||
|
|
||||||
.value_filter {
|
.value_filter {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
|
@ -1,3 +1,12 @@
|
|||||||
|
/** 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
|
||||||
|
**/
|
||||||
|
|
||||||
var fileLast = 0;
|
var fileLast = 0;
|
||||||
function renderFile(cnt, type="", file_extracted="", file_plain=""){
|
function renderFile(cnt, type="", file_extracted="", file_plain=""){
|
||||||
var template = $("#filesTemplate").html();
|
var template = $("#filesTemplate").html();
|
||||||
|
@ -1,3 +1,12 @@
|
|||||||
|
/** 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
|
||||||
|
**/
|
||||||
|
|
||||||
function pagination_link(name, value){
|
function pagination_link(name, value){
|
||||||
let link_args = db_args;
|
let link_args = db_args;
|
||||||
if (name && value){
|
if (name && value){
|
||||||
|
@ -1,3 +1,12 @@
|
|||||||
|
{#-
|
||||||
|
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
|
||||||
|
-#}
|
||||||
<div class="modal" tabindex="-1" id="message_sent">
|
<div class="modal" tabindex="-1" id="message_sent">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
Reference in New Issue
Block a user