21 Commits
v0.1 ... v0.7

Author SHA1 Message Date
63784119d2 Update Docs
All checks were successful
Build and push Docker image at git tag / build (push) Successful in 38m31s
2024-10-29 23:58:46 +01:00
533b9fed6d Agent should work 2024-10-29 23:57:04 +01:00
17d96cd069 Basic MGMT should work 2024-10-29 18:09:27 +01:00
98198f105b Update Docs 2024-10-29 16:51:37 +01:00
fac784e013 Management Message Processing 2024-10-29 16:47:58 +01:00
04ccd488f8 Web Interface ok 2024-10-29 15:22:32 +01:00
86adf41308 New Riddle (w/o Data!!) 2024-10-09 20:48:06 +02:00
6de994626b Update Docs 2024-10-09 19:04:28 +02:00
5e0945f9ae Accept Messages 2024-10-09 19:04:19 +02:00
4199b1c347 Table works 2024-10-09 15:13:40 +02:00
a4d0803d20 Basic Table
All checks were successful
Build and push Docker image at git tag / build (push) Successful in 1m44s
2024-10-08 21:00:14 +02:00
e376956def Fix DB && Logs to Stdout in Docker
All checks were successful
Build and push Docker image at git tag / build (push) Successful in 1m54s
2024-10-08 14:11:57 +02:00
afc35be35a Better docs
All checks were successful
Build and push Docker image at git tag / build (push) Successful in 7m7s
2024-10-08 13:20:04 +02:00
28eee676c4 Begin Interface 2024-10-08 13:08:17 +02:00
cff39d61de DB works 2024-10-08 12:44:35 +02:00
d36ebf9694 Begin DB 2024-10-07 19:48:00 +02:00
b889b581f2 Messages via Types with Validation etc. 2024-10-06 15:09:55 +02:00
4ea424e0d1 Begin Types for Messages 2024-10-05 15:07:50 +02:00
9d0cd7e89b Devmode 2024-10-04 22:43:27 +02:00
00633347f4 Persists Path
All checks were successful
Build and push Docker image at git tag / build (push) Successful in 10m34s
2024-10-04 22:20:32 +02:00
ee01751170 Enable push
All checks were successful
Build and push Docker image at git tag / build (push) Successful in 2m26s
2024-10-04 21:37:35 +02:00
72 changed files with 13496 additions and 26 deletions

View File

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

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
__pycache__
/data/*
/bin/
/lib/
/include/
/pyvenv.cfg

View File

@ -1,2 +1,27 @@
# Agenten-Plattform # Agenten-Plattform
## Management
- `./docker-mgmt/`
- `./ums/management/`
- `./web/`
- `./build-mgmt.sh`
## Basic Agent
- `./docker-agent/`
- `./ums/agent/`
- `./build-agent.sh`
## Development
### Run via Docker
- `docker compose up`
### VS Code Autocomplete ...
- `python3 -m venv .` (only once)
- `source ./bin/activate`
- `pip install requests fastapi pdoc` (only once)
- Select Python from `./bin/python` in VS Code

50
build-agent.sh Executable file
View 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;

View File

@ -1,5 +1,15 @@
#/bin/bash #/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 # https://stackoverflow.com/a/4774063
SCRIPTPATH="$(cd -- "$(dirname "$0")" >/dev/null 2>&1; pwd -P)" SCRIPTPATH="$(cd -- "$(dirname "$0")" >/dev/null 2>&1; pwd -P)"

View File

@ -1,5 +1,35 @@
ARG FROM_IMAGE= # 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
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"]

View 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:
# ...

View 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

53
docker-compose.yml Normal file
View File

@ -0,0 +1,53 @@
# 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
# This file is for development!!
# docker compose up
# See https://git.chai.uni-hamburg.de/UMS-Agenten/Agent-Template for production usage!
services:
management:
image: git.chai.uni-hamburg.de/ums-agenten/management:arm64
#image: git.chai.uni-hamburg.de/ums-agenten/management:amd64
ports:
- 8000:80
environment:
- SOLUTION_MAX_TRIALS=5
- MANAGEMENT_URL=http://management
- AGENTS_PROCESS=http://agent_all:8000
- AGENTS_SOLVE=http://agent_all:8000
- AGENTS_GATEKEEPER=http://agent_all:8000
volumes:
- ./data/share/:/ums-agenten/share/
- ./data/persist-management/:/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 "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"

View File

@ -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
FROM ubuntu:24.04 FROM ubuntu:24.04
ARG H_GID ARG H_GID
@ -26,7 +36,7 @@ RUN ln -s /usr/bin/python3 /usr/local/bin/python \
&& addgroup --gid $H_GID user \ && addgroup --gid $H_GID user \
&& adduser user --uid $H_UID --ingroup user --gecos "" --home /home/user/ --disabled-password && adduser user --uid $H_UID --ingroup user --gecos "" --home /home/user/ --disabled-password
RUN mkdir -p /ums-agenten/plattform/ RUN mkdir -p /ums-agenten/plattform/ && mkdir -p /ums-agenten/persist/
COPY ./docker-mgmt/$PIP_REQ_FILE /ums-agenten/requirements.txt COPY ./docker-mgmt/$PIP_REQ_FILE /ums-agenten/requirements.txt
RUN pip3 install --break-system-packages --no-cache-dir -r /ums-agenten/requirements.txt \ RUN pip3 install --break-system-packages --no-cache-dir -r /ums-agenten/requirements.txt \

View File

@ -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
server { server {
listen 80 default_server; listen 80 default_server;
@ -7,6 +17,14 @@ server {
root /ums-agenten/plattform/web/public/; root /ums-agenten/plattform/web/public/;
index index.html; index index.html;
location = / {
server_name_in_redirect off;
port_in_redirect off;
absolute_redirect off;
return 303 /index;
}
location / { location / {
try_files $uri $uri/ @dynamic; try_files $uri $uri/ @dynamic;
} }

View File

@ -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
user user; user user;
worker_processes auto; worker_processes auto;
pid /run/nginx.pid; pid /run/nginx.pid;

View File

@ -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
# non frozen dependecies, to use latest # non frozen dependecies, to use latest
requests requests
tqdm tqdm

View File

@ -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
from setuptools import find_packages, setup from setuptools import find_packages, setup
setup( setup(

View File

@ -1,6 +1,18 @@
# 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
[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"
@ -10,6 +22,7 @@ autorestart=false
[fcgi-program:uvicorn] [fcgi-program:uvicorn]
socket=unix:///tmp/uvicorn.sock socket=unix:///tmp/uvicorn.sock
environment=SERVE=true
command=/usr/local/bin/uvicorn ums.management.main:app --uds /tmp/uvicorn.sock --proxy-headers command=/usr/local/bin/uvicorn ums.management.main:app --uds /tmp/uvicorn.sock --proxy-headers
numprocs=4 numprocs=4
process_name=uvicorn-%(process_num)d process_name=uvicorn-%(process_num)d
@ -18,6 +31,11 @@ autostart=true
autorestart=true autorestart=true
priority=10 priority=10
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
[program:nginx] [program:nginx]
command=/usr/sbin/nginx -g 'daemon off;' command=/usr/sbin/nginx -g 'daemon off;'
autostart=true autostart=true

17
docs.sh Executable file
View File

@ -0,0 +1,17 @@
#!/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
pdoc ./ums/ \
--output-directory ./web/public/docs/ \
--no-browser \
--docformat google \
--template-directory ./utils/doc-template

View File

@ -1,5 +1,15 @@
#/bin/bash #/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 # https://stackoverflow.com/a/4774063
SCRIPTPATH="$(cd -- "$(dirname "$0")" >/dev/null 2>&1; pwd -P)" SCRIPTPATH="$(cd -- "$(dirname "$0")" >/dev/null 2>&1; pwd -P)"
@ -24,11 +34,11 @@ do
echo " $IMAGE_REGISTRY/$IMAGE_OWNER/$image_name:$image_tag" echo " $IMAGE_REGISTRY/$IMAGE_OWNER/$image_name:$image_tag"
echo " $IMAGE_REGISTRY/$IMAGE_OWNER/$image_name:$image_tag-$day_tag" echo " $IMAGE_REGISTRY/$IMAGE_OWNER/$image_name:$image_tag-$day_tag"
#docker push "$IMAGE_REGISTRY/$IMAGE_OWNER/$image_name:$image_tag" docker push "$IMAGE_REGISTRY/$IMAGE_OWNER/$image_name:$image_tag"
#docker tag "$IMAGE_REGISTRY/$IMAGE_OWNER/$image_name:$image_tag" \ docker tag "$IMAGE_REGISTRY/$IMAGE_OWNER/$image_name:$image_tag" \
# "$IMAGE_REGISTRY/$IMAGE_OWNER/$image_name:$image_tag-$day_tag" "$IMAGE_REGISTRY/$IMAGE_OWNER/$image_name:$image_tag-$day_tag"
#docker push "$IMAGE_REGISTRY/$IMAGE_OWNER/$image_name:$image_tag-$day_tag" docker push "$IMAGE_REGISTRY/$IMAGE_OWNER/$image_name:$image_tag-$day_tag"
fi; fi;
done done

View File

View 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

View File

@ -0,0 +1,18 @@
# 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 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

9
ums/example/__init__.py Normal file
View 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

42
ums/example/__main__.py Normal file
View File

@ -0,0 +1,42 @@
# 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
if __name__ == "__main__":
## Example: Sending messages to management via python
from ums.utils import AgentMessage, RiddleData, RiddleDataType, RiddleSolution, ManagementRequest
ex = AgentMessage(
id="ex5",
riddle={
"context":"Example 1",
"question":"Get the name of the person."
},
data=[
RiddleData(
type=RiddleDataType.TEXT,
file_plain="./cv.txt"
)
]
)
ex.status.extract.required = False
ex.solution = RiddleSolution(
solution="Otto",
explanation="Written in line 6 after 'Name:'"
)
mr = ManagementRequest("localhost")
print(mr.send_message(ex))
print(mr.get_status(20))

71
ums/example/example.py Normal file
View 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
]

View 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

196
ums/management/db.py Normal file
View File

@ -0,0 +1,196 @@
# 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 sqlite3, atexit
from datetime import datetime
from threading import Lock
from typing import Generator
from pydantic import validate_call, ValidationError
from ums.utils import PERSIST_PATH, AgentMessage, MessageDbRow
class DB():
_DB_TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
def __init__(self):
self.db = sqlite3.connect(
os.path.join(PERSIST_PATH, 'messages.db'),
check_same_thread=False
)
self.db.row_factory = sqlite3.Row
atexit.register(lambda db : db.close(), self.db)
self.db_lock = Lock()
self._assure_tables()
def _assure_tables(self):
self.db_lock.acquire()
with self.db:
self.db.execute("""CREATE TABLE IF NOT EXISTS Messages (
count INTEGER PRIMARY KEY AUTOINCREMENT,
id TEXT,
sender TEXT,
recipient TEXT,
time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
json BLOB,
processed BOOL DEFAULT FALSE,
solution BOOL DEFAULT NULL
)""")
self.db_lock.release()
@validate_call
def add_message(self, sender:str, recipient:str, message:AgentMessage, processed:bool=False ) -> int:
self.db_lock.acquire()
with self.db:
self.db.execute(
"""INSERT INTO Messages (
id, sender, recipient, json, processed
) VALUES (
:id, :sender, :recipient, :json, :processed
)""", {
"id" : message.id,
"sender" : sender,
"recipient" : recipient,
"json" : message.model_dump_json(),
"processed" : processed
})
new_count = self.db.execute("SELECT LAST_INSERT_ROWID() as last").fetchone()
self.db_lock.release()
return new_count['last']
@validate_call
def set_processed(self, count:int, processed:bool=True) -> bool:
self.db_lock.acquire()
with self.db:
try:
self.db.execute("UPDATE Messages SET processed = ? WHERE count = ?", (processed, count))
return True
except:
return False
finally:
self.db_lock.release()
@validate_call
def set_solution(self, count:int, solution:bool) -> bool:
self.db_lock.acquire()
with self.db:
try:
self.db.execute("UPDATE Messages SET solution = ? WHERE count = ?", (solution, count))
return True
except:
return False
finally:
self.db_lock.release()
def __iter__(self) -> Generator[MessageDbRow, None, None]:
yield from self.iterate()
@validate_call
def iterate(self,
id:str|None=None, sender:str|None=None, recipient:str|None=None,
processed:bool|None=None, solution:bool|None=None,
time_after:int|None=None, time_before:int|None=None,
limit:int=20, offset:int=0, _count_only:bool=False
) -> Generator[MessageDbRow|int, None, None]:
where = []
params = {
"lim": limit,
"off": offset
}
for v,n in (
(id,'id'),
(sender,'sender'), (recipient,'recipient'),
(processed,'processed'), (solution,'solution')
):
if not v is None:
where.append('{} = :{}'.format(n,n))
params[n] = v
if time_after:
where.append("time > :t_after")
params['t_after'] = datetime.fromtimestamp(time_after).strftime(self._DB_TIME_FORMAT)
if time_before:
where.append("time < :t_before")
params['t_before'] = datetime.fromtimestamp(time_before).strftime(self._DB_TIME_FORMAT)
if len(where) > 0:
where_clause = "WHERE " + (' AND '.join(where))
else:
where_clause = ""
with self.db:
if _count_only:
count = self.db.execute(
"SELECT COUNT(*) as count FROM Messages {}".format(where_clause),
params
).fetchone()
yield count['count']
else:
for row in self.db.execute(
"SELECT * FROM Messages {} ORDER BY time DESC LIMIT :lim OFFSET :off".format(where_clause),
params
):
yield self._create_row_object(row, allow_lazy=True)
def __len__(self) -> int:
return self.len()
def len(self, **kwargs) -> int:
"""
See `DB.iterate` for possible values of `kwargs`.
"""
kwargs['_count_only'] = True
return next(self.iterate(**kwargs))
def _create_row_object(self, row:sqlite3.Row, allow_lazy:bool=True) -> MessageDbRow:
try:
message = AgentMessage.model_validate_json(
row['json'],
context={"require_file_exists": not allow_lazy}
)
except ValidationError as e:
if allow_lazy:
message = AgentMessage(
id="error",
riddle={"context":str(e),"question":"Failed to load from Database!"}
)
else:
raise e
return MessageDbRow(
count=row['count'],
sender=row['sender'],
recipient=row['recipient'],
time=int(datetime.strptime(row['time'], self._DB_TIME_FORMAT).timestamp()),
message=message,
processed=row['processed'],
solution=row['solution']
)
def by_count(self, count:int) -> MessageDbRow|None:
with self.db:
try:
return self._create_row_object(
self.db.execute("SELECT * FROM Messages WHERE count = ?", (count,)).fetchone()
)
except:
return None

106
ums/management/interface.py Normal file
View File

@ -0,0 +1,106 @@
# 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 re
from urllib.parse import urlencode
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from ums.management.db import DB
from ums.utils import (AgentMessage, RiddleDataType,
list_shared_data, list_shared_schema, SHARE_PATH)
class Interface():
_PREFIX = "/app"
def __init__(self, template:Jinja2Templates, db:DB):
self.template = template
self.db = db
self.router = APIRouter(
prefix=self._PREFIX,
tags=["app, gui"]
)
self._add_routes()
def _add_routes(self):
@self.router.get("/", response_class=RedirectResponse, summary="Redirect")
def index(request: Request) -> RedirectResponse:
return RedirectResponse(self._PREFIX + "/table")
@self.router.get("/table", response_class=HTMLResponse, summary="Table of messages")
def table(request: Request,
id:str|None=None, sender:str|None=None, recipient:str|None=None,
processed:bool|None=None, solution:bool|None=None,
time_after:int|str|None=None, time_before:int|str|None=None,
limit:int=10, offset:int=0, _count_only:bool=False
):
db_args = {
"limit" : limit,
"offset" : offset
}
convert_time = lambda t: self.template.env.globals["date2timestamp"](t) \
if not re.match(r'^\d+$', t) else int(t)
for v,n,f in (
(id,'id',str), (sender,'sender',str), (recipient,'recipient',str),
(processed,'processed', bool), (solution,'solution', bool),
(time_after, 'time_after', convert_time), (time_before, 'time_before', convert_time)
):
if not v is None:
db_args[n] = f(v)
if _count_only:
return self.db.len(**db_args)
else:
def pagination_link(**kwargs):
link_args = db_args.copy()
link_args.update(kwargs)
return urlencode(link_args)
return self.template.TemplateResponse(
'table.html',
{"request" : request,
"db" : self.db, "db_args" : db_args,
"pagination_link" : pagination_link
}
)
@self.router.get("/table/total", summary="Total number of messages in table")
def table_total(request: Request,
id:str|None=None, sender:str|None=None, recipient:str|None=None,
processed:bool|None=None, solution:bool|None=None,
time_after:int|str|None=None, time_before:int|str|None=None,
limit:int=10, offset:int=0
) -> int:
kwargs = locals().copy()
del kwargs['table']
kwargs['_count_only'] = True
return table(**kwargs)
@self.router.get("/new", response_class=HTMLResponse, summary="Add new riddle")
def new(request: Request):
return self.template.TemplateResponse(
'new.html',
{"request" : request,
"AgentMessage" : AgentMessage, "RiddleDataType": RiddleDataType,
"shared_data" : list_shared_data(), "shared_schema" : list_shared_schema(),
"SHARE_PATH" : SHARE_PATH
}
)

View File

@ -1,17 +1,102 @@
# 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
# TEST ONLY import os
from typing import Union from datetime import datetime
from fastapi import FastAPI from fastapi import FastAPI, Request, BackgroundTasks, HTTPException
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
app = FastAPI() from jinja2.runtime import Undefined as JinjaUndefined
@app.get("/") from ums.management.interface import Interface
def read_root(): from ums.management.db import DB, MessageDbRow
return {"Hello": "World"} from ums.management.process import MessageProcessor
from ums.utils import AgentMessage, AgentResponse, TEMPLATE_PATH
class WebMain():
_TIME_FORMAT = "%H:%M:%S %d.%m.%Y"
def __init__(self):
self._init_app()
self._init_templates()
self.db = DB()
self.msg_process = MessageProcessor(self.db)
self._add_routes()
self._add_routers()
@app.get("/items/{item_id}") def _init_app(self):
def read_item(item_id: int, q: Union[str, None] = None): self.app = FastAPI(
return {"item_id": item_id, "q": q} title="Agenten Plattform",
description="Agenten Plattform Management",
openapi_url="/api/schema.json",
docs_url='/api',
redoc_url=None
)
def _init_templates(self):
self.template = Jinja2Templates(
directory=TEMPLATE_PATH,
auto_reload=True
)
def timestamp2date(t:int|JinjaUndefined) -> str:
return "" if isinstance(t, JinjaUndefined) \
else datetime.fromtimestamp(t).strftime(self._TIME_FORMAT)
self.template.env.globals["timestamp2date"] = timestamp2date
def date2timestamp(d:str|JinjaUndefined) -> int|str:
return "" if isinstance(d, JinjaUndefined) \
else int(datetime.strptime(d, self._TIME_FORMAT).timestamp())
self.template.env.globals["date2timestamp"] = date2timestamp
def _add_routers(self):
interface_router = Interface(self.template, self.db)
self.app.include_router(interface_router.router)
def _add_routes(self):
@self.app.get("/index", response_class=HTMLResponse, summary="Link list")
def index(request: Request):
return self.template.TemplateResponse(
'index.html',
{"request" : request}
)
@self.app.post("/message", summary="Send a message to the management")
def message(request: Request, message:AgentMessage, background_tasks: BackgroundTasks) -> AgentResponse:
receiver = request.headers['host']
if ':' in receiver:
receiver = receiver[:receiver.rindex(':')]
sender = request.headers['x-forwarded-for']
return self.msg_process.new_message(sender, receiver, message, background_tasks)
@self.app.get("/status", summary="Get status of a message")
def status(count:int) -> MessageDbRow:
msg = self.db.by_count(count)
if msg is None:
raise HTTPException(status_code=404, detail="Message not found")
return msg
if __name__ == "ums.management.main" and os.environ.get('SERVE', 'false') == 'true':
main = WebMain()
app = main.app

165
ums/management/process.py Normal file
View File

@ -0,0 +1,165 @@
# 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, re
from typing import List
import requests
from fastapi import BackgroundTasks
from ums.management.db import DB
from ums.utils import AgentMessage, AgentResponse, logger
class MessageProcessor():
SOLUTION_MAX_TRIALS = int(os.environ.get('SOLUTION_MAX_TRIALS', 5))
MANAGEMENT_URL = os.environ.get('MANAGEMENT_URL', 'http://127.0.0.1:80').strip().strip('/')
AGENTS_PROCESS = tuple(map(
lambda s:s.strip().strip('/'),
os.environ.get('AGENTS_PROCESS', '').split(',')
))
AGENTS_SOLVE = tuple(map(
lambda s:s.strip().strip('/'),
os.environ.get('AGENTS_SOLVE', '').split(',')
))
AGENTS_GATEKEEPER = tuple(map(
lambda s:s.strip().strip('/'),
os.environ.get('AGENTS_GATEKEEPER', '').split(',')
))
def __init__(self, db:DB):
self.db = db
self.management_name = self._get_name(self.MANAGEMENT_URL)
if len(self.AGENTS_PROCESS) == 0:
logger.warning(f"Not Process Agent (AGENTS_PROCESS) found, this may be a problem!")
if len(self.AGENTS_SOLVE) == 0:
logger.warning(f"Not Solve Agent (AGENTS_SOLVE) found, this may be a problem!")
if len(self.AGENTS_GATEKEEPER) == 0:
logger.warning(f"Not Gatekeeper Agent (AGENTS_GATEKEEPER) found, this may be a problem!")
def _get_name(self, url:str) -> str:
m = re.match(r'^https?://([^:]*)(?::(\d+))?$', url)
return "unknown" if m == None else m.group(1)
def new_message(self,
sender:str, receiver:str, message:AgentMessage,
background_tasks: BackgroundTasks
) -> AgentResponse:
try:
db_count = self.db.add_message(sender, receiver, message)
background_tasks.add_task(self._process_message, db_count)
return AgentResponse(
count=db_count,
msg="Added message to queue"
)
except Exception as e:
return AgentResponse(
count=-1,
error=True,
error_msg=str(e)
)
def _process_message(self, count:int, ignore_processed:bool=False):
db_message = self.db.by_count(count)
if db_message.processed and not ignore_processed:
# do not process processed messages again
return
# check which step/ state the message requires the management to do
if db_message.message.status.extract.required and not db_message.message.status.extract.finished:
# send to extract agents
self._send_messages(self.AGENTS_PROCESS, db_message.message)
elif db_message.message.status.solve.required and not db_message.message.status.solve.finished:
# send to solve agents
self._send_messages(self.AGENTS_SOLVE, db_message.message)
elif db_message.message.status.validate.required and not db_message.message.status.validate.finished:
# send to solve agents
self._send_messages(self.AGENTS_GATEKEEPER, db_message.message)
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:
# yay, message is solved
self.db.set_solution(count=count, solution=True);
else:
# not solved, but all steps done
self.db.set_solution(count=count, solution=False);
# try again
self._do_again(db_message.message)
# now message processed!
self.db.set_processed(count=count, processed=True)
def _do_again(self, message:AgentMessage):
if message.status.trial < self.SOLUTION_MAX_TRIALS:
# try again, recycle message
# require steps again
if message.status.extract.required:
message.status.extract.finished = False
if message.status.solve.required:
message.status.solve.finished = False
if message.status.validate.required:
message.status.validate.finished = False
# increment trial
message.status.trial += 1
# append current solution als old one
if not message.solution is None:
message.riddle.solutions_before.append(
message.solution
)
# reset current solution
message.solution = None
# add the riddle as new to management
self._send_message(self.MANAGEMENT_URL, message)
def _send_messages(self, recipients:List[str], message:AgentMessage) -> bool:
ok = True
for r in recipients:
ok = ok and self._send_message(r, message)
return ok
def _send_message(self, recipient:str, message:AgentMessage) -> bool:
db_count = self.db.add_message(
sender=self.management_name,
recipient=self._get_name(recipient),
message=message,
processed=False
)
r = requests.post(
"{}/message".format(recipient),
data=message.model_dump_json(),
headers={"accept" : "application/json", "content-type" : "application/json"}
)
if r.status_code == 200:
self.db.set_processed(db_count, processed=True)
return True
else:
logger.warning(f"Error sending message to: {recipient} {(r.text, r.headers)}")
return False

40
ums/utils/__init__.py Normal file
View File

@ -0,0 +1,40 @@
# 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 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 (
RiddleInformation,
AgentMessage,
Riddle,
RiddleSolution,
RiddleData,
RiddleDataType,
RiddleStatus,
AgentResponse,
MessageDbRow
)
from ums.utils.request import ManagementRequest
from ums.utils.functions import list_shared_data, list_shared_schema

25
ums/utils/const.py Normal file
View File

@ -0,0 +1,25 @@
# 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
"""
This file contains shared constants.
See the content ...
"""
import os, logging
BASE_PATH = '/ums-agenten'
SHARE_PATH = os.path.join(BASE_PATH, 'share')
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')
LOG_FILE = os.path.join(PERSIST_PATH, 'ums.log')
LOG_LEVEL = logging.INFO

56
ums/utils/functions.py Normal file
View File

@ -0,0 +1,56 @@
# 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 typing import List, Callable
from ums.utils.const import SHARE_PATH
def list_path(path:str) -> List[str]:
if os.path.isdir(path):
items = []
for item in os.listdir(path):
full = os.path.join(path, item)
if os.path.isdir(full):
items.extend(list_path(full))
elif os.path.isfile(full):
items.append(full)
return items
else:
return []
def list_shared(filter:Callable=lambda p,n,e: True) -> List[str]:
files = []
for f in list_path(SHARE_PATH):
r = f[len(SHARE_PATH)+1:]
if r[0] == '.' or '/.' in r:
# hidden files
continue
if '/' in r and '.' in r:
path, name, ending = r[:r.rfind('/')], r[r.rfind('/')+1:r.rfind('.')], r[r.rfind('.')+1:]
elif '/' in r:
path, name, ending = r[:r.rfind('/')], r[r.rfind('/')+1:], ""
elif '.' in r:
path, name, ending = "", r[:r.rfind('.')], r[r.rfind('.')+1:]
else:
path, name, ending = "", r, ""
if filter(path, name, ending):
files.append(r)
return files
def list_shared_data():
return list_shared(lambda p,n,e: e != "json")
def list_shared_schema():
return list_shared(lambda p,n,e: e == "json")

45
ums/utils/request.py Normal file
View File

@ -0,0 +1,45 @@
# 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 requests
from ums.utils.types import AgentMessage, AgentResponse, MessageDbRow
class RequestException(Exception):
pass
class ManagementRequest():
def __init__(self, hostname:str, port:int=80):
self.url = "http://{hostname}:{port}".format(hostname=hostname, port=port)
def get_status(self, count:int) -> MessageDbRow:
r = requests.get(
"{}/status".format(self.url),
params={"count": count}
)
if r.status_code == 200:
return MessageDbRow.model_validate_json(r.text)
else:
raise RequestException(str(r.text)+str(r.headers))
def send_message(self, message:AgentMessage) -> AgentResponse:
r = requests.post(
"{}/message".format(self.url),
data=message.model_dump_json(),
headers={"accept" : "application/json", "content-type" : "application/json"}
)
if r.status_code == 200:
return AgentResponse.model_validate_json(r.text)
else:
return AgentResponse(count=-1, error=True, error_msg=str(r.text)+str(r.headers))

379
ums/utils/types.py Normal file
View File

@ -0,0 +1,379 @@
# 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
"""
This represents the basic types used to interact with the management.
The types are implemented using [pydantic](https://docs.pydantic.dev/).
It provides validation, allow JSON serialization and works well with [FastAPI](https://fastapi.tiangolo.com/) which is used internally for the http request between the agents and the management.
### Example
```python
ex = AgentMessage(
id="ex1",
riddle={
"context":"Example 1",
"question":"Get the name of the person."
},
data=[
RiddleData(
type=RiddleDataType.TEXT,
file_plain="./cv.txt"
)
]
)
ex.status.extract.required = False
```
```json
{
"id": "ex1",
"sub_ids": [],
"riddle": {
"context": "Example 1",
"question": "Get the name of the person.",
"solutions_before": []
},
"solution": null,
"data": [
{
"type": "text",
"file_plain": "/ums-agenten/share/cv.txt",
"file_extracted": null
}
],
"status": {
"extract": {
"required": false,
"finished": false
},
"solve": {
"required": true,
"finished": false
},
"validate": {
"required": true,
"finished": false
},
"trial": 0,
"solved": false
}
}
```
```python
ex.solution = RiddleSolution(
solution="Otto",
explanation="Written in line 6 after 'Name:'"
)
```
```json
{
...
"solution": {
"solution": "Otto",
"explanation": "Written in line 6 after 'Name:'",
"used_data": [],
"accepted": false,
"review": null
},
...
}
```
"""
import os
from enum import Enum
from typing import List, Any
from typing_extensions import Annotated
from pydantic import (
BaseModel,
ValidationError, ValidationInfo,
ValidatorFunctionWrapHandler
)
from pydantic.functional_validators import WrapValidator, AfterValidator
from ums.utils.const import SHARE_PATH
class RiddleInformation(BaseModel):
"""
This is the basic class used as superclass for all message and infos
about a riddle.
"""
class RiddleDataType(Enum):
"""
Enum for the three types of data used in a riddle.
"""
TEXT = "text"
IMAGE = "image"
AUDIO = "audio"
def _check_data_file(file_name:str) -> str:
if not file_name.startswith('/'):
file_name = os.path.join(SHARE_PATH, file_name)
assert file_name.startswith(SHARE_PATH), "The data file needs to be in {}!".format(SHARE_PATH)
file_name = os.path.realpath(file_name, strict=False)
assert os.path.isfile(file_name), "The data file {} does not exist!".format(file_name)
return file_name
def _ignore_file_missing(
v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
) -> str:
try:
return handler(v)
except ValidationError:
if not info.context is None and \
"require_file_exists" in info.context and \
info.context["require_file_exists"] == False and \
isinstance(v, str):
return "missing:{}".format(v)
else:
raise
class RiddleData(RiddleInformation):
"""
A data item to be used to solve the riddle
"""
type: RiddleDataType
"""
The type of the data item.
"""
file_plain: Annotated[str, AfterValidator(_check_data_file), WrapValidator(_ignore_file_missing)]
"""
The plain file (as path to file system) without any processing.
The path will be validated and must start with `SHARE_PATH` (or be relative to `SHARE_PATH`).
The file must exist.
"""
file_extracted: Annotated[str, AfterValidator(_check_data_file), WrapValidator(_ignore_file_missing)] | None = None
"""
The processed files (as path to file system), i.e., a schematic file containing all extracted informations.
The path will be validated and must start with `SHARE_PATH` (or be relative to `SHARE_PATH`).
The file must exist.
"""
class RiddleSolution(RiddleInformation):
"""
A solution of a riddle.
"""
solution: str
"""
The textual value of the solution.
"""
explanation: str
"""
An explanation of the solution.
"""
used_data: List[RiddleData] = []
"""
The data items used to create the solution (optional).
"""
accepted : bool = False
"""
If the solution is accepted by validator/ gatekeeper.
"""
review: str | None = None
"""
A review of the solution (if None: not tried to validate)
"""
class Riddle(RiddleInformation):
"""
The riddle (the task description and possibly a solution)
"""
context: str
"""
The context of the riddle (as textual string).
"""
question: str
"""
The actual main question of the riddle (as textual string).
"""
solutions_before: List[RiddleSolution] = []
"""
If already tried to solve this riddle before, the (not accepted) solutions are stored here
"""
class RiddleSubStatus(RiddleInformation):
"""
The sub status for each possible step a riddle may go though.
"""
required: bool = True
"""
Is this step required (i.e., requested)
"""
finished: bool = False
"""
Was this step already executed.
"""
class RiddleStatus(RiddleInformation):
"""
The status of a riddle, will be mostly changed by Management when the riddle is sent to different agents while solving it.
"""
extract: RiddleSubStatus = RiddleSubStatus()
"""
The first extract step (image, text, audio -> more sematic data)
The `RiddleData` items in `AgentMessage.data` shall have `file_extracted` afterwards.
"""
solve: RiddleSubStatus = RiddleSubStatus()
"""
The *main* solving step.
`AgentMessage.solution` shall be an `RiddleSolution` afterwards.
"""
validate: RiddleSubStatus = RiddleSubStatus()
"""
The validation step, i.e., does the gatekeeper accept the solution in `AgentMessage.solution`.
"""
trial: int = 0
"""
A counter for the number of trials.
Each time the gatekeeper does not accept a solution of this riddle, the value is incremented.
"""
solved: bool = False
"""
True, after the gatekeeper accepts the solution at `AgentMessage.solution`
"""
class AgentMessage(RiddleInformation):
"""
The basic message, which is sent be the agent and the management.
The objects will be JSON en- and decoded.
"""
id: str
"""
The riddle id, e.g., ``ex1``
This is a unique string and identifies the riddle.
"""
sub_ids: List[str] = []
"""
There might be cases, when an agent decided to split in riddle in multiple *smaller* steps.
Each *sub* riddle will then get its own id (i.e., ``ex1-sub1``) while the sub id is added here as reference.
"""
riddle: Riddle
"""
The riddle to solve.
"""
solution: RiddleSolution | None = None
"""
The solution of the riddle (or empty if no solution available)
"""
data: List[RiddleData] = []
"""
The data to get the solution from.
"""
status: RiddleStatus = RiddleStatus()
"""
The status of the riddle.
"""
class AgentResponse(RiddleInformation):
"""
Returned by the management when receiving an `AgentMessage`.
"""
count : int
"""
The count of the message (overall numeric id).
"""
msg: str|None = None
"""
An additional message.
"""
error: bool = False
"""
If an error occurred.
"""
error_msg: str|None = None
"""
Error message (if `error` )
"""
class MessageDbRow(BaseModel):
"""
Object representing a database row.
"""
count : int
"""
The count (primary key) of the item.
"""
sender : str
"""
The sender of the message.
"""
recipient : str
"""
The recipient of the message
"""
time : int
"""
The time (unix timestamp) the message was received/ sent.
"""
message : AgentMessage
"""
The message received/ sent.
"""
processed : bool
"""
Did the management process the message, i.e., did the tasks necessary for this message (mostly only relevant for received messages).
"""
solution : bool|None = None
"""
Does this message contain a valid solution?
True if contains valid solution, False if solution not valid, Null/None if not applicable
"""

View File

@ -0,0 +1,20 @@
{# 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
#}
{% extends "default/module.html.jinja2" %}
{% macro is_public(doc) %}
{% if doc.name in ("model_config", "model_fields", "model_computed_fields") %}
{% else %}
{{ default_is_public(doc) }}
{% endif %}
{% endmacro %}

11
vars.sh
View File

@ -1,7 +1,18 @@
#/bin/bash #/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
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"

View File

@ -0,0 +1,7 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="0; url=./ums.html"/>
</head>
</html>

46
web/public/docs/search.js Normal file

File diff suppressed because one or more lines are too long

250
web/public/docs/ums.html Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,8 +0,0 @@
<html>
<head>
<title>UMS-Agenten Management</title>
</head>
<body>
<h1>Empty</h1>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

6
web/public/static/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
web/public/static/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View 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
**/
.value_filter {
width: 150px;
}
#message_content{
height: 300px;
}

126
web/public/static/new.js Normal file
View File

@ -0,0 +1,126 @@
/** 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;
function renderFile(cnt, type="", file_extracted="", file_plain=""){
var template = $("#filesTemplate").html();
template = template.replaceAll('{cnt}', cnt)
.replaceAll('{file_extracted}', file_extracted)
.replaceAll('{file_extracted}', file_extracted)
.replaceAll('{file_plain}', file_plain);
['text', 'image', 'audio'].forEach((t)=>{
template = template.replaceAll('{'+t+'_selected}', t === type ? 'selected' : '' );
});
$("#filesRender").append(template);
$("button#removeFile"+cnt).click( () => {
$("#filesRow" + cnt).remove();
update_json_message()
});
fileLast = cnt;
}
$("#addFile").click(() => renderFile(++fileLast));
function update_json_message(){
let message = JSON.parse(basic_message);
var store_values = {}
$(".message-attribute").each((_,v)=>{
let el = $(v);
let name = el.attr('name');
let val = el.attr("type") == "checkbox" ? el.prop('checked') : el.val();
// optional fields (empty => null)
if( name.endsWith("file_extracted") && val.length == 0 ){
val = null;
}
store_values[name] = val;
let curr_msg = message;
let last_ob = {};
let last_name = "";
name.split('.').forEach((e)=>{
last_ob = curr_msg;
last_name = e;
if( !curr_msg.hasOwnProperty(e) ){
curr_msg[e] = {}
}
curr_msg = curr_msg[e];
});
last_ob[last_name] = val;
});
localStorage.setItem("new_riddle", JSON.stringify(store_values))
// fix array for data
if(message.hasOwnProperty('data')){
var data_items = message['data'];
message['data'] = [];
Object.keys(data_items).forEach((v)=>{
if( v !== null){
message['data'].push(data_items[v]);
}
});
}
$("#message_content").val(JSON.stringify(message, null, 2));
}
function load_last_values(){
if(localStorage.getItem("new_riddle") !== null){
var items = JSON.parse(localStorage.getItem("new_riddle"))
console.log(items)
Object.keys(items).forEach((k)=>{
// if data, create the file select for this id
if(k.startsWith('data.') && $("[name='"+k+"']").length == 0){
renderFile(parseInt(k.substring(5, k.indexOf('.',5))))
}
let el = $("[name='"+k+"']");
if(el.attr("type") == "checkbox"){
el.prop('checked', items[k]);
}
else{
el.val(items[k]);
}
});
update_json_message()
}
}
load_last_values();
$( document ).on( "change", ".message-attribute", update_json_message)
$( document ).on( "click", ".message-attribute[type=checkbox]", update_json_message)
function send_json_message(json_str){
$.ajax(
"/message",
{
contentType : 'application/json',
type : 'POST',
data: json_str,
},
).then((d) => {
$("#message_sent .modal-title").text("Message Sent");
$("#message_sent .modal-body").html('<pre>'+JSON.stringify(d, null, 2)+'</pre>');
new bootstrap.Modal('#message_sent').show();
}).fail((d)=>{
$("#message_sent .modal-title").text("Error Sending Message");
$("#message_sent .modal-body").html('<pre>'+JSON.stringify(d, null, 2)+'</pre>');
new bootstrap.Modal('#message_sent').show();
});
}
$("#send_message").click(
() => send_json_message($("#message_content").val())
);

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
**/
function pagination_link(name, value){
let link_args = db_args;
if (name && value){
link_args[name] = value;
}
else if(name){
delete link_args[name];
}
return '?' + $.param( link_args );
}
$(".value_filter").change((e)=>{
window.location = pagination_link($(e.target).attr('name').substr('filter_'.length), $(e.target).val())
});
function enable_auto_refresh(){
setInterval( () => {
if($('#autoRefresh').prop('checked')){
$.get(
'/app/table/total' + pagination_link(),
(v) => {
if( v != db_total ){
window.location = pagination_link()
}
}
);
}
}, 1000);
sessionStorage.setItem('auto_refresh', $('#autoRefresh').prop('checked'));
}
$("#autoRefresh").click(enable_auto_refresh);
if(sessionStorage.hasOwnProperty('auto_refresh') && sessionStorage.getItem('auto_refresh') === 'true'){
$("#autoRefresh").prop('checked', true);
enable_auto_refresh()
}
function send_json_message(json_str){
$.ajax(
"/message",
{
contentType : 'application/json',
type : 'POST',
data: json_str,
},
).then((d) => {
$("#message_sent .modal-title").text("Message Sent");
$("#message_sent .modal-body").html('<pre>'+JSON.stringify(d, null, 2)+'</pre>');
new bootstrap.Modal('#message_sent').show();
}).fail((d)=>{
$("#message_sent .modal-title").text("Error Sending Message");
$("#message_sent .modal-body").html('<pre>'+JSON.stringify(d, null, 2)+'</pre>');
new bootstrap.Modal('#message_sent').show();
});
}
$(".send_message_again").click(
(e) => send_json_message( $("pre#row_message_raw_"+$(e.target).attr('idx')).text() )
);

49
web/templates/base.html Normal file
View File

@ -0,0 +1,49 @@
{#-
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
-#}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<link rel="stylesheet" href="/static/bootstrap.min.css" />
<link rel="stylesheet" href="/static/main.css" />
<script src="/static/jquery.min.js"></script>
<script src="/static/bootstrap.bundle.min.js"></script>
<title>{{title}} &ndash; MGMT</title>
<!--
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
-->
{% block morehead %}
{% endblock %}
</head>
<body>
<div class="container">
<h1>{{title}} &ndash; MGMT</h1>
{% block maincontent %}
{% endblock %}
</div>
{% block morefoot %}
{% endblock %}
</body>
</html>

19
web/templates/index.html Normal file
View File

@ -0,0 +1,19 @@
{#-
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
-#}
{% extends "base.html" %}
{% set title = "Overview" %}
{% block maincontent %}
<ul>
<li><a href="/app/table" target="_blank">&nearr; Web App: Table</a></li>
<li><a href="/app/new" target="_blank">&nearr; Web App: New Riddle</a></li>
<li><a href="/api/" target="_blank">&nearr; Documentation: API </a></li>
<li><a href="/docs/" target="_blank">&nearr; Documentation: Code </a></li>
</ul>
{% endblock %}

21
web/templates/modal.html Normal file
View File

@ -0,0 +1,21 @@
{#-
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-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
</div>
</div>
</div>
</div>

129
web/templates/new.html Normal file
View File

@ -0,0 +1,129 @@
{#-
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
-#}
{% extends "base.html" %}
{% set title = "New" %}
{% block maincontent %}
<div class="row">
<div class="col">
</div>
<div class="col">
<a href="/app/table" class="btn btn-secondary">&larr; Back to Messages</a>
</div>
</div>
<h2>Create New Riddle</h2>
<div class="mb-3">
<label for="message_id" class="form-label">Riddle ID</label>
<input type="text" name="id" class="message-attribute form-control" id="message_id">
</div>
<div class="mb-3">
<label for="message_id" class="form-label">Riddle Question</label>
<input type="text" name="riddle.question" class="message-attribute form-control" id="message_id">
</div>
<div class="mb-3">
<label for="message_context" class="form-label">Riddle Context</label>
<textarea class="form-control message-attribute" name="riddle.context" id="message_context" rows="2"></textarea>
</div>
<h3>Steps</h3>
<div class="mb-3">
<div class="form-check form-check-inline">
<input class="form-check-input message-attribute" name="status.extract.required" type="checkbox" id="message_extract" value="true" checked>
<label class="form-check-label" for="message_extract">Extract</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input message-attribute" name="status.solve.required" type="checkbox" id="message_solve" value="true" checked>
<label class="form-check-label" for="message_solve">Solve</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input message-attribute" name="status.validate.required" type="checkbox" id="message_validate" value="true" checked>
<label class="form-check-label" for="message_validate">Validate</label>
</div>
</div>
<h3>Files</h3>
<p>
Manually add files to the shared directory and then select them here to assign them to a riddle.
Shared directory is <code>{{SHARE_PATH}}</code> which should be linked to the host using docker to, e.g., <code>./data/share</code>.
</p>
<datalist id="dataFiles">
{% for f in shared_data %}
<option value="{{f}}">
{% endfor %}
</datalist>
<datalist id="schemaFiles">
{% for f in shared_schema %}
<option value="{{f}}">
{% endfor %}
</datalist>
<template id="filesTemplate">
<div class="mb-3 row" id="filesRow{cnt}">
<div class="col col-sm-1">
{cnt}
</div>
<div class="col">
<label for="fileType{cnt}" class="form-label">File Type</label>
<select id="fileType{cnt}" name="data.{cnt}.type" class="form-select message-attribute">
<option>Select Type</option>
{% for t in RiddleDataType %}
<option value="{{t.value}}" { {{- t.value -}} _selected}>{{t.name}}</option>
{% endfor %}
</select>
</div>
<div class="col">
<label for="inputFile{cnt}" class="form-label">Input File</label>
<input class="form-control message-attribute" name="data.{cnt}.file_plain"
list="dataFiles" id="inputFile{cnt}" value="{file_plain}" placeholder="Type to search files">
</div>
<div class="col">
<label for="schemaFile{cnt}" class="form-label">Schema File</label>
<input class="form-control message-attribute" name="data.{cnt}.file_extracted"
list="schemaFiles" id="schemaFile{cnt}" value="{file_extracted}" placeholder="Type to search files">
</div>
<div class="col col-sm-1">
<button type="button" id="removeFile{cnt}" class="btn btn-danger">Remove</button>
</div>
</div>
</template>
<div id="filesRender">
</div>
<div class="mb-3 row">
<div class="col col-sm-1">
<button type="button" id="addFile" class="btn btn-secondary">Add</button>
</div>
</div>
<h2>JSON Representation</h2>
<div class="mb-3">
<div class="form-floating">
<textarea class="form-control" id="message_content">
{{- AgentMessage(id="",riddle={"context":"","question":""}).model_dump_json(indent=2) -}}
</textarea>
<label for="message_content">Message to send</label>
</div>
</div>
<button type="submit" class="btn btn-primary" id="send_message">Send Message</button>
{% include "modal.html" %}
{% endblock %}
{% block morehead %}
<script>
const basic_message = '{{ AgentMessage(id="",riddle={"context":"","question":""}).model_dump_json()|safe }}';
</script>
{% endblock %}
{% block morefoot %}
<script src="/static/new.js"></script>
{% endblock %}

144
web/templates/table.html Normal file
View File

@ -0,0 +1,144 @@
{#-
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
-#}
{% extends "base.html" %}
{% set title = "Messages" %}
{% macro pagination() %}
<nav>
<ul class="pagination justify-content-center">
<li class="page-item {% if db_args.offset-db_args.limit < 0 %}disabled{% endif %}">
<a class="page-link" href="?{{ pagination_link(offset=db_args.offset-db_args.limit) }}">Previous</a>
</li>
<li class="page-item active" aria-current="page">
<span class="page-link">Offset: {{db_args.offset}}</span>
</li>
<li class="page-item">
<a class="page-link" href="?{{ pagination_link(offset=db_args.offset+db_args.limit) }}">Next</a>
</li>
</ul>
</nav>
{% endmacro %}
{% block maincontent %}
<div class="row">
<div class="col">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="autoRefresh">
<label class="form-check-label" for="autoRefresh">Refresh for new messages</label>
</div>
</div>
<div class="col">
{{pagination()}}
</div>
<div class="col">
<a href="/app/new" class="btn btn-secondary">&rarr; Add a Riddle</a>
</div>
</div>
<table class="table table-striped">
<thead>
{% for item in db.iterate(**db_args) %}
{% set field_names = ['id'] + item.__fields__.keys()|list %}
{% if loop.index == 1 %}
<tr id="row_0">
{% for field in field_names %}
<th>
{% if field == 'time' %}
<input type="text" class="value_filter" name="filter_time_before" value="{{timestamp2date(db_args.time_before)}}" class="form-control" placeholder="Before"><br />
<input type="text" class="value_filter" name="filter_time_after" value="{{timestamp2date(db_args.time_after)}}" class="form-control" placeholder="After"><br />
{% elif field not in ('message', 'count') %}
<input type="text" class="value_filter" name="filter_{{field}}" value="{{db_args[field]}}" class="form-control" placeholder="Filter"><br />
{% endif %}
{{ field.title() }}
</th>
{% endfor %}
</tr>
</thead><tbody>
{% endif %}
<tr id="row_{{loop.index}}">
{% set row_index = loop.index %}
{% for field in field_names %}
{% if field == "message" %}
<td>
<button type="button" class="btn btn-outline-secondary btn-outline" data-bs-toggle="modal" data-bs-target="#row_message_{{row_index}}">
Show Message
</button>
<div class="modal fade" id="row_message_{{row_index}}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5">Content of Message</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<pre id="row_message_raw_{{row_index}}">{{ item[field].model_dump_json(indent=2)|string }}</pre>
<button class="btn btn-warning send_message_again" idx="{{row_index}}">Send Again</button>
</div>
</div>
</div>
</div>
</td>
{% elif field == "id" %}
<td>{{ item.message.id }}</td>
{% elif field == "time" %}
<td ts="item[field]">{{ timestamp2date(item[field]) }}</td>
{% else %}
<td>{{ item[field] }}</td>
{% endif %}
{% endfor %}
</tr>
{% else %}
<div class="alert alert-warning" role="alert">
No items found, reset offset, limit, filter, ...!<br />
<a class="btn btn-warning" href="/app/table">Reset</a>
</div>
{% endfor %}
</tbody>
</table>
<div class="row">
<div class="col">
{{pagination()}}
</div>
<div class="col">
<div class="input-group">
<label class="input-group-text" for="total-items">Total items</label>
<input class="form-control" id="total-items" disabled value="{{ db.len(**db_args) }}" />
</div>
</div>
<div class="col">
<div class="input-group">
<label class="input-group-text" for="items-per-page">Items per page</label>
<select class="form-select" id="items-per-page" onchange="window.location = this.value;">
<option value="?{{ pagination_link(limit=5) }}" {% if db_args.limit == 5 %}selected{% endif %}>5</option>
<option value="?{{ pagination_link(limit=10) }}" {% if db_args.limit == 10 %}selected{% endif %}>10</option>
<option value="?{{ pagination_link(limit=25) }}" {% if db_args.limit == 25 %}selected{% endif %}>25</option>
<option value="?{{ pagination_link(limit=100) }}" {% if db_args.limit == 100 %}selected{% endif %}>100</option>
{% if db_args.limit not in (5, 10, 25, 100) %}
<option value="?{{ pagination_link(limit=db_args.limit) }}" selected>{{db_args.limit}}</option>
{% endif %}
</select>
</div>
</div>
</div>
{% include "modal.html" %}
{% endblock %}
{% block morehead %}
<script>
const db_args = JSON.parse('{{ db_args|tojson }}');
const db_total = {{ db.len(**db_args) }};
</script>
{% endblock %}
{% block morefoot %}
<script src="/static/table.js"></script>
{% endblock %}