Compare commits
No commits in common. "master" and "v0.2" have entirely different histories.
@ -1,4 +1,4 @@
|
|||||||
name: Build and push Docker images on git tags
|
name: Build and push Docker image at git tag
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
@ -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
|
||||||
|
10
.gitignore
vendored
10
.gitignore
vendored
@ -1,10 +0,0 @@
|
|||||||
__pycache__
|
|
||||||
|
|
||||||
# data of containers
|
|
||||||
/data/*
|
|
||||||
|
|
||||||
# ignore local venv
|
|
||||||
/bin/
|
|
||||||
/lib/
|
|
||||||
/include/
|
|
||||||
/pyvenv.cfg
|
|
38
Readme.md
38
Readme.md
@ -1,40 +1,2 @@
|
|||||||
> [!NOTE]
|
|
||||||
> In diesem Repository befinden sich die Implementierung des Management und der Agenten-Plattform. Sowie Skripte zur Erstellung der Docker-Images.
|
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> Um die Plattform zu benutzen, bitte das [Agent-Template](https://git.chai.uni-hamburg.de/UMS-Agenten/Agent-Template) benutzen!
|
|
||||||
|
|
||||||
# Agenten-Plattform
|
# Agenten-Plattform
|
||||||
|
|
||||||
## Management
|
|
||||||
Verzeichnisse insb.:
|
|
||||||
- `./utils/mgmt/` Docker container configs
|
|
||||||
- `./ums/management/` Python source
|
|
||||||
- `./web/` Jinja templates and web root
|
|
||||||
- `./build-mgmt.sh` Container build script
|
|
||||||
|
|
||||||
|
|
||||||
## Basic Agent
|
|
||||||
Verzeichnisse insb.:
|
|
||||||
- `./utils/agent/` Docker container configs
|
|
||||||
- `./ums/agent/` Python source
|
|
||||||
- `./build-agent.sh` Container build script
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
### Run via Docker
|
|
||||||
- `docker compose up`
|
|
||||||
|
|
||||||
### CLI Examples
|
|
||||||
- Requests to management
|
|
||||||
- `docker compose exec management python -m ums.example` (runs file `ums/example/__main__.py`)
|
|
||||||
- Run single task in agent
|
|
||||||
- `docker compose exec agent_all python -m ums.agent -h`
|
|
||||||
|
|
||||||
### VS Code Autocomplete
|
|
||||||
(In VS Code)
|
|
||||||
|
|
||||||
- `python3 -m venv .` (only once)
|
|
||||||
- `source ./bin/activate`
|
|
||||||
- `pip install requests fastapi pdoc` (only once)
|
|
||||||
- Select Python from `./bin/python` in VS Code
|
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
#/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/utils/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/utils/agent/requirements-frozen.txt"
|
|
||||||
docker rm "$cid"
|
|
||||||
fi;
|
|
@ -1,15 +1,5 @@
|
|||||||
#/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)"
|
||||||
|
|
||||||
@ -29,7 +19,7 @@ for platform in $PLATFORMS; do
|
|||||||
docker build \
|
docker build \
|
||||||
--pull \
|
--pull \
|
||||||
--platform "linux/$platform" \
|
--platform "linux/$platform" \
|
||||||
--file "$SCRIPTPATH/utils/mgmt/Dockerfile" \
|
--file "$SCRIPTPATH/docker-mgmt/Dockerfile" \
|
||||||
--build-arg H_UID=1050 \
|
--build-arg H_UID=1050 \
|
||||||
--build-arg PIP_REQ_FILE="$requirements" \
|
--build-arg PIP_REQ_FILE="$requirements" \
|
||||||
--build-arg H_GID=1050 \
|
--build-arg H_GID=1050 \
|
||||||
@ -41,6 +31,6 @@ done;
|
|||||||
if [ "$requirements" == "requirements.txt" ]; then
|
if [ "$requirements" == "requirements.txt" ]; then
|
||||||
# extract requirements-frozen.txt
|
# extract requirements-frozen.txt
|
||||||
cid=$(docker create "$IMAGE_REGISTRY/$IMAGE_OWNER/$IMAGE_NAME_MGMT:arm64")
|
cid=$(docker create "$IMAGE_REGISTRY/$IMAGE_OWNER/$IMAGE_NAME_MGMT:arm64")
|
||||||
docker cp "$cid:/ums-agenten/requirements.txt" "$SCRIPTPATH/utils/mgmt/requirements-frozen.txt"
|
docker cp "$cid:/ums-agenten/requirements.txt" "$SCRIPTPATH/docker-mgmt/requirements-frozen.txt"
|
||||||
docker rm "$cid"
|
docker rm "$cid"
|
||||||
fi;
|
fi;
|
||||||
|
5
docker-agent/Dockerfile
Normal file
5
docker-agent/Dockerfile
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
ARG FROM_IMAGE=
|
||||||
|
|
||||||
|
FROM $FROM_IMAGE
|
||||||
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
|||||||
# 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
|
|
||||||
- MESSAGE_MAX_CONTACTS=100
|
|
||||||
- REQUIRE_FULL_EXTRACT=true
|
|
||||||
- REQUIRE_FULL_SOLVE=true
|
|
||||||
- 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"
|
|
||||||
|
|
@ -1,13 +1,3 @@
|
|||||||
# 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
|
||||||
@ -36,19 +26,19 @@ 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/ && mkdir -p /ums-agenten/persist/
|
RUN mkdir -p /ums-agenten/plattform/
|
||||||
|
|
||||||
COPY ./utils/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 \
|
||||||
&& pip3 freeze > /ums-agenten/requirements.txt
|
&& pip3 freeze > /ums-agenten/requirements.txt
|
||||||
|
|
||||||
# nginx settings and startup
|
# nginx settings and startup
|
||||||
COPY ./utils/mgmt/supervisor.conf /etc/supervisor/supervisord.conf
|
COPY ./docker-mgmt/supervisor.conf /etc/supervisor/supervisord.conf
|
||||||
COPY ./utils/mgmt/nginx.conf /etc/nginx/nginx.conf
|
COPY ./docker-mgmt/nginx.conf /etc/nginx/nginx.conf
|
||||||
COPY ./utils/mgmt/app.conf /etc/nginx/sites-enabled/default
|
COPY ./docker-mgmt/app.conf /etc/nginx/sites-enabled/default
|
||||||
|
|
||||||
# install the code of the repo
|
# install the code of the repo
|
||||||
COPY ./utils/setup.py /ums-agenten/plattform/
|
COPY ./docker-mgmt/setup.py /ums-agenten/plattform/
|
||||||
RUN pip3 install --break-system-packages -e /ums-agenten/plattform/
|
RUN pip3 install --break-system-packages -e /ums-agenten/plattform/
|
||||||
|
|
||||||
WORKDIR /ums-agenten/plattform/ums/
|
WORKDIR /ums-agenten/plattform/ums/
|
@ -1,13 +1,3 @@
|
|||||||
# 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;
|
||||||
@ -17,23 +7,10 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /share {
|
|
||||||
alias /ums-agenten/share;
|
|
||||||
autoindex on;
|
|
||||||
}
|
|
||||||
|
|
||||||
location @dynamic {
|
location @dynamic {
|
||||||
proxy_set_header Host $http_host;
|
proxy_set_header Host $http_host;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
@ -1,13 +1,3 @@
|
|||||||
# 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;
|
7
docker-mgmt/requirements.txt
Normal file
7
docker-mgmt/requirements.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# non frozen dependecies, to use latest
|
||||||
|
requests
|
||||||
|
tqdm
|
||||||
|
pdoc
|
||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
python-multipart
|
9
docker-mgmt/setup.py
Normal file
9
docker-mgmt/setup.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='ums',
|
||||||
|
packages=find_packages(),
|
||||||
|
version='0.0.0',
|
||||||
|
description='UMS-Agenten',
|
||||||
|
author='Magnus Bender',
|
||||||
|
)
|
@ -1,18 +1,6 @@
|
|||||||
# 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"
|
||||||
@ -22,7 +10,6 @@ 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
|
||||||
@ -31,11 +18,6 @@ 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
17
docs.sh
@ -1,17 +0,0 @@
|
|||||||
#!/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/ ./ums/example/__main__.py \
|
|
||||||
--output-directory ./web/public/docs/ \
|
|
||||||
--no-browser \
|
|
||||||
--docformat google \
|
|
||||||
--template-directory ./utils/doc-template
|
|
@ -1,15 +1,5 @@
|
|||||||
#/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)"
|
||||||
|
|
||||||
@ -20,15 +10,14 @@ day_tag=$(date '+%Y-%m-%d')
|
|||||||
images_a=$(docker image ls "$IMAGE_REGISTRY/$IMAGE_OWNER/$IMAGE_NAME_AGENT" --format '{{.Repository}}:{{.Tag}}')
|
images_a=$(docker image ls "$IMAGE_REGISTRY/$IMAGE_OWNER/$IMAGE_NAME_AGENT" --format '{{.Repository}}:{{.Tag}}')
|
||||||
images_b=$(docker image ls "$IMAGE_REGISTRY/$IMAGE_OWNER/$IMAGE_NAME_MGMT" --format '{{.Repository}}:{{.Tag}}')
|
images_b=$(docker image ls "$IMAGE_REGISTRY/$IMAGE_OWNER/$IMAGE_NAME_MGMT" --format '{{.Repository}}:{{.Tag}}')
|
||||||
|
|
||||||
echo "$images_a
|
echo "$images_a\n$images_b" | while read image_url ;
|
||||||
$images_b" | while read image_url ;
|
|
||||||
do
|
do
|
||||||
|
|
||||||
image_name="${image_url##*/}"
|
image_name="${image_url##*/}"
|
||||||
image_name="${image_name%%:*}"
|
image_name="${image_name%%:*}"
|
||||||
image_tag="${image_url##*:}"
|
image_tag="${image_url##*:}"
|
||||||
|
|
||||||
if [[ "$image_tag" =~ ^((gpu-)|(cpu-))?((arm64)|(amd64))$ ]];
|
if [[ "$image_tag" =~ ^((gpu)|(cpu)-)?((arm64)|(amd64))$ ]];
|
||||||
then
|
then
|
||||||
|
|
||||||
echo "Push:"
|
echo "Push:"
|
||||||
|
0
ums/__cli__.py
Normal file
0
ums/__cli__.py
Normal file
@ -1,34 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
"""
|
|
||||||
The package `ums` contains the Agenten-Plattform, the implementations of the agents shall be created in the package `src`, see [Agent-Template](https://git.chai.uni-hamburg.de/UMS-Agenten/Agent-Template).
|
|
||||||
|
|
||||||
> Side note: The classes with comments may be useful when implementing the agents.
|
|
||||||
> The classes without comments may be safe to ignore and are (only) used internally.
|
|
||||||
|
|
||||||
- `ums.agent`
|
|
||||||
- Contains the implementation of an agent for handling requests by the implementations in `src`.
|
|
||||||
- Check for running single tasks without using management.
|
|
||||||
- `ums.example`
|
|
||||||
- Contains a very simple examples for all types of agents.
|
|
||||||
- See `ums.example.example`
|
|
||||||
- `ums.management`
|
|
||||||
- Contains the implementation of the management.
|
|
||||||
- Take a look at the web gui of the management, possibly at <http://localhost:8080/> or <http://localhost:8000/>
|
|
||||||
- `ums.utils`
|
|
||||||
- Contains various utilities.
|
|
||||||
- `ums.utils.const.SHARE_PATH` The path for shared files between all agents
|
|
||||||
- `ums.utils.const.PERSIST_PATH` The path to store persistent data of an agent
|
|
||||||
- `ums.utils.request.ManagementRequest` Run request to the management (only necessary in special cases, most requests done automatically by platform)
|
|
||||||
- `ums.utils.schema` The schema (types) used in the files storing extracted data from plain data
|
|
||||||
- `ums.utils.types` The types used in the communication between agent and management
|
|
||||||
|
|
||||||
"""
|
|
@ -1,53 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
## Run as Agent
|
|
||||||
|
|
||||||
The env. variable `AGENTS_LIST` is used to identify the agents classes/ task handlers.
|
|
||||||
It must contain he the package name and a variable name in this package divided by `:`.
|
|
||||||
Then, the variable contains a list of agent classes (subclasses of `ums.agent.agent.BasicAgent`)
|
|
||||||
|
|
||||||
For example `AGENTS_LIST=ums.example.example:AGENT_CLASSES`, then in file `./ums/example/example.py` a variable `AGENT_CLASSES` exists.
|
|
||||||
One line in this file, e.g., is `AGENT_CLASSES = [MyExtractAudioAgent, MyExtractImageAgent]`.
|
|
||||||
|
|
||||||
When starting the Docker container of the agent, the classes specified in `AGENTS_LIST` are loaded and if the agent receives a task, the task is sent to the agent classes' `handle` methods.
|
|
||||||
|
|
||||||
## Run Single Task
|
|
||||||
|
|
||||||
For development it might be cumbersome to always require a running management container and sending messages.
|
|
||||||
Hence, tasks can be run manually from the terminal (still in the container and using the agent classes), but without having a management.
|
|
||||||
|
|
||||||
This also uses the `AGENTS_LIST` env. variable, but the tasks are sent via command line:
|
|
||||||
|
|
||||||
There are three ways to send a task (if the agent's Docker container is running):
|
|
||||||
- `docker compose exec agent_all python -m ums.agent -d`
|
|
||||||
- Run a dummy task
|
|
||||||
- Possibly `agent_all` needs to be changed to the service name (see `docker-compose.yml`) of the agent's Docker container
|
|
||||||
- `cat ./msg.json | docker compose exec -T agent_all python -m ums.agent -i`
|
|
||||||
- Send the task (json of `AgentMessage`) via STDIN from file `./msg.json` to the agent
|
|
||||||
- `docker compose exec agent_all python -m ums.agent -f msg.json`
|
|
||||||
- Get the task from the json file, the files are searched for by full name, in the shared, and the persistent directory.
|
|
||||||
|
|
||||||
If the Agent's Docker container is not running, a temporary container can be started.
|
|
||||||
For the dummy message, the command would be `docker compose run --rm --entrypoint "" agent_all python -m ums.agent -d`.
|
|
||||||
(Again, change `agent_all` for the service name in `docker-compose.yml`.)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from ums.agent.agent import (
|
|
||||||
AgentCapability,
|
|
||||||
BasicAgent,
|
|
||||||
ExtractAgent,
|
|
||||||
ExtractAudioAgent, ExtractImageAgent, ExtractTextAgent,
|
|
||||||
SolveAgent,
|
|
||||||
GatekeeperAgent
|
|
||||||
)
|
|
@ -1,65 +0,0 @@
|
|||||||
# 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 argparse, sys, os, json
|
|
||||||
|
|
||||||
from ums.agent.process import MessageProcessor
|
|
||||||
from ums.utils import AgentMessage, Riddle, SHARE_PATH, PERSIST_PATH
|
|
||||||
|
|
||||||
class _FakeBackgroundTask():
|
|
||||||
def add_task(self, call, *args):
|
|
||||||
call(*args)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='Agenten Plattform – Run Single Task')
|
|
||||||
parser.add_argument('-f', '--file', help="fetch the message (riddle) from this json file")
|
|
||||||
parser.add_argument('-d', '--dummy', help="use a dummy message (riddle)", action="store_true")
|
|
||||||
parser.add_argument('-i', '--stdin', help="get the message (riddle) from STDIN", action="store_true")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
message = None
|
|
||||||
|
|
||||||
if args.dummy:
|
|
||||||
message = AgentMessage(
|
|
||||||
id="dummy",
|
|
||||||
riddle=Riddle(context="Its a dummy.", question="No question!")
|
|
||||||
)
|
|
||||||
message.status.extract.required = False
|
|
||||||
elif args.stdin:
|
|
||||||
text = ''.join(sys.stdin.readlines())
|
|
||||||
message = AgentMessage.model_validate_json(text)
|
|
||||||
elif args.file:
|
|
||||||
if os.path.isfile(args.file):
|
|
||||||
f_name = args.file
|
|
||||||
elif os.path.isfile(os.path.join(SHARE_PATH, args.file)):
|
|
||||||
f_name = os.path.join(SHARE_PATH, args.file)
|
|
||||||
elif os.path.isfile(os.path.join(PERSIST_PATH, args.file)):
|
|
||||||
f_name = os.path.join(PERSIST_PATH, args.file)
|
|
||||||
else:
|
|
||||||
print()
|
|
||||||
print(f"\tFile {args.file} not found!")
|
|
||||||
print()
|
|
||||||
f_name = None
|
|
||||||
|
|
||||||
if not f_name is None:
|
|
||||||
message = AgentMessage.model_validate(
|
|
||||||
json.load(open(f_name, 'r'))
|
|
||||||
)
|
|
||||||
|
|
||||||
if message is None:
|
|
||||||
parser.print_help()
|
|
||||||
else:
|
|
||||||
mp = MessageProcessor(disable_messages=True)
|
|
||||||
response = mp.new_message(message, _FakeBackgroundTask())
|
|
||||||
|
|
||||||
print("\tResponse:")
|
|
||||||
print(response.model_dump_json(indent=2))
|
|
@ -1,326 +0,0 @@
|
|||||||
# 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 random, os, json, time
|
|
||||||
|
|
||||||
from abc import abstractmethod, ABC
|
|
||||||
from enum import Enum
|
|
||||||
from typing import List, Callable
|
|
||||||
|
|
||||||
from pydantic import validate_call
|
|
||||||
|
|
||||||
from ums.utils import (
|
|
||||||
RiddleInformation, AgentMessage, RiddleDataType, RiddleData, Riddle,
|
|
||||||
RiddleStatus, RiddleSolution,
|
|
||||||
ExtractedData,
|
|
||||||
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
|
|
||||||
|
|
||||||
@validate_call
|
|
||||||
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):
|
|
||||||
# do a very short sleep
|
|
||||||
time.sleep(random.random())
|
|
||||||
|
|
||||||
# sending
|
|
||||||
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}")
|
|
||||||
|
|
||||||
@validate_call
|
|
||||||
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
|
|
||||||
|
|
||||||
@validate_call
|
|
||||||
def message(self) -> AgentMessage:
|
|
||||||
"""
|
|
||||||
Get the message this agent object is working on.
|
|
||||||
"""
|
|
||||||
return self._message;
|
|
||||||
|
|
||||||
@validate_call
|
|
||||||
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
|
|
||||||
|
|
||||||
@validate_call
|
|
||||||
def get_extracted(self, data:RiddleData) -> ExtractedData|None:
|
|
||||||
"""
|
|
||||||
Loads the extracted data from the `data` item (i.e., from the file `data.file_extracted`).
|
|
||||||
|
|
||||||
Returns None if no extracted data found.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not data.file_extracted is None:
|
|
||||||
return ExtractedData.model_validate(
|
|
||||||
json.load(open(data.file_extracted, 'r'))
|
|
||||||
)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
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
|
|
||||||
@validate_call
|
|
||||||
def handle(self, data:RiddleData) -> RiddleData:
|
|
||||||
"""
|
|
||||||
Process the item `data`, create extraction file and return `data` with populated `data.file_extracted`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@validate_call
|
|
||||||
def store_extracted(self, data:RiddleData, extracted:ExtractedData, allow_overwrite:bool=True) -> str:
|
|
||||||
"""
|
|
||||||
Stores the newly extracted data (in `extracted`) from `data` (i.e., `data.file_plain`)
|
|
||||||
and returns the filename to use in `data.file_extracted`.
|
|
||||||
|
|
||||||
If there already exists an extracted file for this `data`, the file will be overwritten if `allow_overwrite=True`.
|
|
||||||
Generally the system will check, if the contents of the current file are equal to the contents to write.
|
|
||||||
File with equal content will not be written again.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# get path and name
|
|
||||||
path_name = data.file_plain[:data.file_plain.rfind('.')]
|
|
||||||
candidate = "{}.json".format(path_name)
|
|
||||||
|
|
||||||
# data to write
|
|
||||||
data = extracted.model_dump_json()
|
|
||||||
|
|
||||||
# check for file
|
|
||||||
if os.path.isfile(candidate):
|
|
||||||
|
|
||||||
# get current content
|
|
||||||
with open(candidate, 'r') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
# files equal -> no need to rewrite
|
|
||||||
if content == data:
|
|
||||||
return candidate
|
|
||||||
|
|
||||||
# not equal and overwrite not allowed
|
|
||||||
elif not allow_overwrite:
|
|
||||||
# get non-existent file name
|
|
||||||
cnt = 0
|
|
||||||
while os.path.isfile(candidate):
|
|
||||||
cnt += 1
|
|
||||||
candidate = "{}-{}.json".format(path_name, cnt)
|
|
||||||
|
|
||||||
# write file
|
|
||||||
with open(candidate, 'w+') as f:
|
|
||||||
f.write(data)
|
|
||||||
|
|
||||||
return candidate
|
|
||||||
|
|
||||||
|
|
||||||
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.append(solution)
|
|
||||||
self._response.status.solve.finished = True
|
|
||||||
|
|
||||||
self._do_response = True
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
@validate_call
|
|
||||||
def handle(self, riddle: Riddle, data: List[RiddleData]) -> RiddleSolution:
|
|
||||||
"""
|
|
||||||
Solve the `riddle` using `data` and return a single solution.
|
|
||||||
"""
|
|
||||||
|
|
||||||
class GatekeeperAgent(BasicAgent):
|
|
||||||
"""
|
|
||||||
A gatekeeper agent, create a subclass for your agent.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def agent_capability() -> AgentCapability:
|
|
||||||
return AgentCapability.GATEKEEPER
|
|
||||||
|
|
||||||
def _process(self):
|
|
||||||
if len(self._response.solution) == 0:
|
|
||||||
self._response.solution.append(RiddleSolution(solution="", explanation=""))
|
|
||||||
|
|
||||||
logger.debug(f"Start validate: {self._response.id}")
|
|
||||||
solution = self.handle(self._response.solution, self._response.riddle)
|
|
||||||
|
|
||||||
for single_solution in solution:
|
|
||||||
logger.debug(f"End validate: {self._response.id} ({single_solution.review}, {single_solution.accepted})")
|
|
||||||
if single_solution.review is None or len(single_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 = any(single_solution.accepted for single_solution in solution)
|
|
||||||
|
|
||||||
self._do_response = True
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
@validate_call
|
|
||||||
def handle(self, solution:List[RiddleSolution], riddle:Riddle) -> List[RiddleSolution]:
|
|
||||||
"""
|
|
||||||
Check the `solution` (multiple if multiple solver involved) of `riddle` and return solutions with populated `solution[i].accepted` and `solution[i].review`.
|
|
||||||
"""
|
|
@ -1,65 +0,0 @@
|
|||||||
# 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').lower() == 'true':
|
|
||||||
main = WebMain()
|
|
||||||
app = main.app
|
|
@ -1,100 +0,0 @@
|
|||||||
# 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, disable_messages:bool=False):
|
|
||||||
self.counts = 0
|
|
||||||
self.disable_messages = disable_messages
|
|
||||||
|
|
||||||
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:
|
|
||||||
if not self.disable_messages:
|
|
||||||
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
|
|
||||||
else:
|
|
||||||
print("\tMessages disabled: Requested to send message to management:")
|
|
||||||
print(message.model_dump_json(indent=2))
|
|
@ -1,9 +0,0 @@
|
|||||||
# 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
|
|
@ -1,60 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
"""
|
|
||||||
See the source →
|
|
||||||
"""
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
|
|
||||||
from ums.utils import ManagementRequest
|
|
||||||
|
|
||||||
m_request = ManagementRequest()
|
|
||||||
|
|
||||||
# get infos from Management
|
|
||||||
|
|
||||||
print(
|
|
||||||
# message number 12
|
|
||||||
m_request.get_message(count=12)
|
|
||||||
)
|
|
||||||
|
|
||||||
print(
|
|
||||||
# first two messages of id "test"
|
|
||||||
m_request.list_messages(id="test", limit=2)
|
|
||||||
)
|
|
||||||
|
|
||||||
print(
|
|
||||||
# count messages with id "test"
|
|
||||||
m_request.total_messages(id="test")
|
|
||||||
)
|
|
||||||
|
|
||||||
from ums.utils import AgentMessage, Riddle, RiddleData, RiddleDataType, RiddleSolution
|
|
||||||
|
|
||||||
# send messages to management
|
|
||||||
|
|
||||||
# basic message
|
|
||||||
msg = AgentMessage(
|
|
||||||
id="example",
|
|
||||||
riddle=Riddle(context="Today is the 1. January 1970", question="What time is it?"),
|
|
||||||
data=[
|
|
||||||
RiddleData(
|
|
||||||
type=RiddleDataType.TEXT,
|
|
||||||
file_plain="./cv.txt" # make sure this file exists!
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
# disable some steps
|
|
||||||
msg.status.extract.required = False
|
|
||||||
msg.status.validate.required = False
|
|
||||||
|
|
||||||
print(
|
|
||||||
# send the message
|
|
||||||
m_request.send_message(msg)
|
|
||||||
)
|
|
@ -1,79 +0,0 @@
|
|||||||
# 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 random
|
|
||||||
|
|
||||||
from typing import Callable, List
|
|
||||||
from ums.agent import ExtractAudioAgent, ExtractImageAgent, ExtractTextAgent, SolveAgent, GatekeeperAgent
|
|
||||||
|
|
||||||
from ums.utils import AgentMessage, Riddle, RiddleData, RiddleSolution, RiddleStatus, ExtractedData
|
|
||||||
|
|
||||||
"""
|
|
||||||
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)
|
|
||||||
|
|
||||||
extracted = ExtractedData(other={"info":"just a test"})
|
|
||||||
data.file_extracted = self.store_extracted(data, extracted)
|
|
||||||
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: List[RiddleData]) -> RiddleSolution:
|
|
||||||
|
|
||||||
for d in data:
|
|
||||||
print(self.get_extracted(d))
|
|
||||||
|
|
||||||
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=f"Blubb, {random.random()}")
|
|
||||||
|
|
||||||
|
|
||||||
class MyGatekeeperAgent(GatekeeperAgent):
|
|
||||||
|
|
||||||
def handle(self, solution: List[RiddleSolution], riddle: Riddle) -> RiddleSolution:
|
|
||||||
solution[0].accepted = True
|
|
||||||
solution[0].review = "Ok"
|
|
||||||
|
|
||||||
return solution
|
|
||||||
|
|
||||||
AGENT_CLASSES = [
|
|
||||||
MyExtractAudioAgent, MyExtractImageAgent, MyExtractTextAgent,
|
|
||||||
MySolveAgent,
|
|
||||||
MyGatekeeperAgent
|
|
||||||
]
|
|
@ -1,9 +0,0 @@
|
|||||||
# 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
|
|
@ -1,195 +0,0 @@
|
|||||||
# 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,
|
|
||||||
autocommit=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, count 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
|
|
@ -1,106 +0,0 @@
|
|||||||
# 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=["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
|
|
||||||
}
|
|
||||||
)
|
|
@ -1,125 +1,17 @@
|
|||||||
# 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
|
# TEST ONLY
|
||||||
|
|
||||||
from typing import List
|
from typing import Union
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from fastapi import FastAPI, Request, BackgroundTasks, HTTPException
|
from fastapi import FastAPI
|
||||||
from fastapi.responses import HTMLResponse
|
|
||||||
from fastapi.templating import Jinja2Templates
|
|
||||||
|
|
||||||
from jinja2.runtime import Undefined as JinjaUndefined
|
app = FastAPI()
|
||||||
|
|
||||||
from ums.management.interface import Interface
|
@app.get("/")
|
||||||
from ums.management.db import DB, MessageDbRow
|
def read_root():
|
||||||
from ums.management.process import MessageProcessor
|
return {"Hello": "World"}
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
def _init_app(self):
|
@app.get("/items/{item_id}")
|
||||||
self.app = FastAPI(
|
def read_item(item_id: int, q: Union[str, None] = None):
|
||||||
title="Agenten Plattform",
|
return {"item_id": item_id, "q": q}
|
||||||
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", tags=['agents'])
|
|
||||||
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("/list", summary="Get list of messages (like table)", tags=["cli, agents"])
|
|
||||||
def list(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=10, offset:int=0
|
|
||||||
) -> List[MessageDbRow]:
|
|
||||||
|
|
||||||
db_args = {
|
|
||||||
"limit" : limit,
|
|
||||||
"offset" : offset
|
|
||||||
}
|
|
||||||
|
|
||||||
for v,n in (
|
|
||||||
(id,'id'), (sender,'sender'), (recipient,'recipient'),
|
|
||||||
(processed,'processed'), (solution,'solution'),
|
|
||||||
(time_after, 'time_after'), (time_before, 'time_before')
|
|
||||||
):
|
|
||||||
if not v is None:
|
|
||||||
db_args[n] = v
|
|
||||||
|
|
||||||
return [row for row in self.db.iterate(**db_args)]
|
|
||||||
|
|
||||||
@self.app.get("/list/single", summary="Get a single message", tags=["cli, agents"])
|
|
||||||
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').lower() == 'true':
|
|
||||||
main = WebMain()
|
|
||||||
app = main.app
|
|
@ -1,268 +0,0 @@
|
|||||||
# 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, RiddleData, RiddleSolution
|
|
||||||
|
|
||||||
class MessageProcessor():
|
|
||||||
|
|
||||||
SOLUTION_MAX_TRIALS = int(os.environ.get('SOLUTION_MAX_TRIALS', 5))
|
|
||||||
MESSAGE_MAX_CONTACTS = int(os.environ.get('MESSAGE_MAX_CONTACTS', 100))
|
|
||||||
|
|
||||||
REQUIRE_FULL_EXTRACT = os.environ.get('REQUIRE_FULL_EXTRACT', 'false').lower() == 'true'
|
|
||||||
REQUIRE_FULL_SOLVE = os.environ.get('REQUIRE_FULL_SOLVE', 'false').lower() == 'true'
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# now message processed!
|
|
||||||
self.db.set_processed(count=count, processed=True)
|
|
||||||
|
|
||||||
# increment contacts counter
|
|
||||||
db_message.message.contacts += 1
|
|
||||||
if db_message.message.contacts > self.MESSAGE_MAX_CONTACTS:
|
|
||||||
logger.warning(f"Message reached max number of contacts! {db_message.message.id}, {count}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# check which step/ state the message requires the management to do
|
|
||||||
# -> IF
|
|
||||||
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)
|
|
||||||
return
|
|
||||||
|
|
||||||
# combine different extractions in data items
|
|
||||||
# will update items in `db_message.message.data`
|
|
||||||
fully_extracted = self._add_extractions(db_message.message.id, db_message.message.data)
|
|
||||||
if self.REQUIRE_FULL_EXTRACT and not fully_extracted:
|
|
||||||
logger.warning(f"Postpone message, wait for full extract of items! {db_message.message.id}, {count}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# -> EL IF
|
|
||||||
if 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)
|
|
||||||
return
|
|
||||||
|
|
||||||
# combine different solutions
|
|
||||||
# will add solutions received before to `db_message.message.solution`
|
|
||||||
fully_solved = self._add_solutions(db_message.message.id, db_message.message.solution, db_message.message.status.trial)
|
|
||||||
if self.REQUIRE_FULL_SOLVE and not fully_solved:
|
|
||||||
logger.warning(f"Postpone message, wait for all solutions of riddle! {db_message.message.id}, {count}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# -> EL IF
|
|
||||||
if 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)
|
|
||||||
return
|
|
||||||
|
|
||||||
# -> 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)
|
|
||||||
|
|
||||||
def _hash_solution(self, s:RiddleSolution) -> int:
|
|
||||||
return hash((s.solution, s.explanation, tuple((d.file_plain, d.type) for d in s.used_data)))
|
|
||||||
|
|
||||||
def _add_solutions(self, riddle_id:str, solution:List[RiddleSolution], trial:int) -> bool:
|
|
||||||
# do not do anything, if all solutions available
|
|
||||||
if len(solution) >= len(self.AGENTS_SOLVE):
|
|
||||||
return True
|
|
||||||
|
|
||||||
contained = set(self._hash_solution(s) for s in solution)
|
|
||||||
|
|
||||||
# search db for solutions from before
|
|
||||||
for row in self.db.iterate(
|
|
||||||
id=riddle_id,
|
|
||||||
limit=min(self.db.len(id=riddle_id), 250)
|
|
||||||
):
|
|
||||||
# make sure to only use solutions from same "trial"
|
|
||||||
if row.message.status.trial == trial:
|
|
||||||
for s in row.message.solution:
|
|
||||||
h = self._hash_solution(s)
|
|
||||||
if h not in contained:
|
|
||||||
# add the 'new' solution
|
|
||||||
solution.append(s)
|
|
||||||
contained.add(h)
|
|
||||||
|
|
||||||
# all solutions found ?
|
|
||||||
if len(solution) >= len(self.AGENTS_SOLVE):
|
|
||||||
break
|
|
||||||
|
|
||||||
return len(solution) >= len(self.AGENTS_SOLVE)
|
|
||||||
|
|
||||||
def _hash_data(self, d:RiddleData) -> int:
|
|
||||||
return hash((d.file_plain, d.type, d.prompt))
|
|
||||||
|
|
||||||
def _add_extractions(self, riddle_id:str, data:List[RiddleData]) -> bool:
|
|
||||||
# get all the data items without extraction
|
|
||||||
empty_data = {}
|
|
||||||
for i, d in enumerate(data):
|
|
||||||
if d.file_extracted is None:
|
|
||||||
empty_data[self._hash_data(d)] = i
|
|
||||||
|
|
||||||
# do not do anything if fully extracted
|
|
||||||
if len(empty_data) == 0:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# search db for extractions already available
|
|
||||||
for row in self.db.iterate(
|
|
||||||
id=riddle_id,
|
|
||||||
limit=min(self.db.len(id=riddle_id), 250)
|
|
||||||
):
|
|
||||||
# check for required extraction
|
|
||||||
for d in row.message.data:
|
|
||||||
# already extracted ?
|
|
||||||
# extraction file exists ?
|
|
||||||
# one of the items, we do not have extractions for ?
|
|
||||||
# the same data item ?
|
|
||||||
if not d.file_extracted is None \
|
|
||||||
and not d.file_extracted.startswith("missing:") \
|
|
||||||
and self._hash_data(d) in empty_data:
|
|
||||||
# copy the reference to the extracted data
|
|
||||||
data[empty_data[self._hash_data(d)]].file_extracted = d.file_extracted
|
|
||||||
# remove from items we need extracted data for
|
|
||||||
del empty_data[self._hash_data(d)]
|
|
||||||
|
|
||||||
# break if all extractions found
|
|
||||||
if len(empty_data) == 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
return len(empty_data) == 0 # fully extracted
|
|
||||||
|
|
||||||
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(s) als old one(s)
|
|
||||||
if len(message.solution) > 0:
|
|
||||||
message.riddle.solutions_before.extend(
|
|
||||||
message.solution
|
|
||||||
)
|
|
||||||
# reset current solution
|
|
||||||
message.solution = []
|
|
||||||
|
|
||||||
# add the riddle as new to management
|
|
||||||
self._send_message(self.MANAGEMENT_URL, message)
|
|
||||||
|
|
||||||
else:
|
|
||||||
logger.info(f"Unsolved riddle after max number of trials: {message.id}")
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
|||||||
# 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').lower() == '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
|
|
||||||
|
|
||||||
from ums.utils.schema import (
|
|
||||||
ExtractionSchema,
|
|
||||||
ExtractedData,
|
|
||||||
ExtractedContent, ExtractedPositions
|
|
||||||
)
|
|
@ -1,25 +0,0 @@
|
|||||||
# 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
|
|
@ -1,56 +0,0 @@
|
|||||||
# 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")
|
|
@ -1,173 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
"""
|
|
||||||
Access to the management, e.g., get the list of messages and single messages.
|
|
||||||
Manually send messages (if necessary, the platforms should do this).
|
|
||||||
|
|
||||||
### Example
|
|
||||||
```python
|
|
||||||
|
|
||||||
m_request = ManagementRequest()
|
|
||||||
|
|
||||||
m_request.get_message(count=12)
|
|
||||||
# MessageDbRow(count=12 sender='from' recipient='to' ...
|
|
||||||
|
|
||||||
m_request.list_messages(id="test", limit=2)
|
|
||||||
# [
|
|
||||||
# MessageDbRow(count=7256, sender='management', ...),
|
|
||||||
# MessageDbRow(count=7255, sender='management', ...),
|
|
||||||
# ]
|
|
||||||
|
|
||||||
m_request.total_messages(id="test")
|
|
||||||
# 31
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
See also `ums.example.__main__` and run in Docker via ``docker compose exec management python -m ums.example``
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from typing import List, Dict, Any
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from pydantic import validate_call
|
|
||||||
|
|
||||||
from ums.utils.types import AgentMessage, AgentResponse, MessageDbRow
|
|
||||||
|
|
||||||
|
|
||||||
class RequestException(Exception):
|
|
||||||
"""
|
|
||||||
Raised on http and similar errors.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ManagementRequest():
|
|
||||||
|
|
||||||
MANAGEMENT_URL = os.environ.get('MANAGEMENT_URL', 'http://127.0.0.1:80').strip().strip('/')
|
|
||||||
|
|
||||||
@validate_call
|
|
||||||
def __init__(self, allow_lazy:bool=True):
|
|
||||||
"""
|
|
||||||
If `allow_lazy` is active, the type checking (by pydantic) is less strict.
|
|
||||||
E.g. it does not require that all files in the data section of messages must exist on the file system.
|
|
||||||
"""
|
|
||||||
self._allow_lazy = allow_lazy
|
|
||||||
self._pydantic_context = {
|
|
||||||
"require_file_exists": not self._allow_lazy
|
|
||||||
}
|
|
||||||
|
|
||||||
@validate_call
|
|
||||||
def get_message(self, count:int) -> MessageDbRow:
|
|
||||||
"""
|
|
||||||
Get a message (like a table row) from the management by using the `count`.
|
|
||||||
|
|
||||||
May raise `RequestException`.
|
|
||||||
"""
|
|
||||||
row = self._get_request(
|
|
||||||
'list/single',
|
|
||||||
{"count": count}
|
|
||||||
)
|
|
||||||
return MessageDbRow.model_validate(
|
|
||||||
row, context=self._pydantic_context
|
|
||||||
)
|
|
||||||
|
|
||||||
@validate_call
|
|
||||||
def list_messages(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=10, offset:int=0
|
|
||||||
) -> List[MessageDbRow]:
|
|
||||||
"""
|
|
||||||
Get the rows in the tables as list of messages.
|
|
||||||
The arguments are used for filtering.
|
|
||||||
|
|
||||||
May raise `RequestException`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
kwargs = locals().copy()
|
|
||||||
params = {}
|
|
||||||
|
|
||||||
for k,v in kwargs.items():
|
|
||||||
if k not in ('self',) and not v is None:
|
|
||||||
params[k] = v
|
|
||||||
|
|
||||||
rows = self._get_request('list', params)
|
|
||||||
|
|
||||||
return [
|
|
||||||
MessageDbRow.model_validate(
|
|
||||||
row, context=self._pydantic_context
|
|
||||||
) for row in rows
|
|
||||||
]
|
|
||||||
|
|
||||||
@validate_call
|
|
||||||
def total_messages(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
|
|
||||||
) -> int:
|
|
||||||
"""
|
|
||||||
Get the total number of rows in the tables matching the filters.
|
|
||||||
|
|
||||||
May raise `RequestException`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
kwargs = locals().copy()
|
|
||||||
params = {}
|
|
||||||
|
|
||||||
for k,v in kwargs.items():
|
|
||||||
if k not in ('self',) and not v is None:
|
|
||||||
params[k] = v
|
|
||||||
|
|
||||||
return int(self._get_request('app/table/total', params))
|
|
||||||
|
|
||||||
def _get_request(self, endpoint:str, params:Dict[str, Any]):
|
|
||||||
r = requests.get(
|
|
||||||
"{}/{}".format(self.MANAGEMENT_URL, endpoint),
|
|
||||||
params=params
|
|
||||||
)
|
|
||||||
|
|
||||||
if r.status_code == 200:
|
|
||||||
return r.json()
|
|
||||||
else:
|
|
||||||
raise RequestException(str(r.text)+"\n"+str(r.headers))
|
|
||||||
|
|
||||||
@validate_call
|
|
||||||
def send_message(self, message:AgentMessage) -> AgentResponse:
|
|
||||||
"""
|
|
||||||
Send the `message` to the management and return the management's agent response.
|
|
||||||
(On error an agent response with error message).
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return AgentResponse.model_validate(
|
|
||||||
self._post_request(
|
|
||||||
"message",
|
|
||||||
message.model_dump_json()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except RequestException as e:
|
|
||||||
return AgentResponse(
|
|
||||||
count=-1,
|
|
||||||
error=True,
|
|
||||||
error_msg=str(e)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _post_request(self, endpoint:str, data:Dict[str, Any]):
|
|
||||||
r = requests.post(
|
|
||||||
"{}/{}".format(self.MANAGEMENT_URL, endpoint),
|
|
||||||
data=data,
|
|
||||||
headers={"accept" : "application/json", "content-type" : "application/json"}
|
|
||||||
)
|
|
||||||
|
|
||||||
if r.status_code == 200:
|
|
||||||
return r.json()
|
|
||||||
else:
|
|
||||||
return RequestException(str(r.text)+"\n"+str(r.headers))
|
|
@ -1,85 +0,0 @@
|
|||||||
# 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 for representing extracted information from the data.
|
|
||||||
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.
|
|
||||||
|
|
||||||
**This is work in progress!**
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import List, Any, Dict
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
class ExtractionSchema(BaseModel):
|
|
||||||
"""
|
|
||||||
This is the basic class used as superclass for all extracted information from data items.
|
|
||||||
|
|
||||||
For all the `ExtractionSchema` is is required that the data can be serialized to json.
|
|
||||||
Thus, mostly only default data types like `int, str, bool, list, dict, tuple` also including `ExtractionSchema` and `RiddleInformation` can be used here!
|
|
||||||
"""
|
|
||||||
|
|
||||||
class ExtractedContent(ExtractionSchema):
|
|
||||||
"""
|
|
||||||
An extracted content item.
|
|
||||||
"""
|
|
||||||
|
|
||||||
type : str
|
|
||||||
"""
|
|
||||||
The type, as a string, the actual string will depend on the extraction agent.
|
|
||||||
"""
|
|
||||||
|
|
||||||
content : str | Any
|
|
||||||
"""
|
|
||||||
The extracted content
|
|
||||||
"""
|
|
||||||
|
|
||||||
class ExtractedPositions(ExtractionSchema):
|
|
||||||
"""
|
|
||||||
A position (like time, coordinates, ...) where something was extracted (each position should belong to a content item).
|
|
||||||
"""
|
|
||||||
|
|
||||||
type : str
|
|
||||||
"""
|
|
||||||
The type, as a string, the actual string will depend on the extraction agent.
|
|
||||||
"""
|
|
||||||
|
|
||||||
position : str | int | Any
|
|
||||||
"""
|
|
||||||
The position, will also depend on the extraction agent.
|
|
||||||
"""
|
|
||||||
|
|
||||||
description : str | Any = None
|
|
||||||
"""
|
|
||||||
An optional description for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
class ExtractedData(ExtractionSchema):
|
|
||||||
"""
|
|
||||||
Contains the extracted items from a data file.
|
|
||||||
"""
|
|
||||||
|
|
||||||
contents : List[ExtractedContent] = []
|
|
||||||
"""
|
|
||||||
The extracted contents (i.e., transcriptions etc.), each item here should belong a position item at the same index.
|
|
||||||
"""
|
|
||||||
|
|
||||||
positions : List[ExtractedPositions] = []
|
|
||||||
"""
|
|
||||||
The positions of extracted contents, each item here should belong a content item at the same index.
|
|
||||||
"""
|
|
||||||
|
|
||||||
other : Dict[str, Any] = {}
|
|
||||||
"""
|
|
||||||
Possibly more data. Use a keywords (depending on agent) and store the data there.
|
|
||||||
"""
|
|
||||||
|
|
@ -1,404 +0,0 @@
|
|||||||
# 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": [],
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"file_plain": "/ums-agenten/share/cv.txt",
|
|
||||||
"file_extracted": null,
|
|
||||||
"prompt": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"status": {
|
|
||||||
"extract": {
|
|
||||||
"required": false,
|
|
||||||
"finished": false
|
|
||||||
},
|
|
||||||
"solve": {
|
|
||||||
"required": true,
|
|
||||||
"finished": false
|
|
||||||
},
|
|
||||||
"validate": {
|
|
||||||
"required": true,
|
|
||||||
"finished": false
|
|
||||||
},
|
|
||||||
"trial": 0,
|
|
||||||
"solved": false
|
|
||||||
},
|
|
||||||
"contacts": 0
|
|
||||||
}
|
|
||||||
```
|
|
||||||
```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, warnings
|
|
||||||
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
from typing import List, Any
|
|
||||||
from typing_extensions import Annotated
|
|
||||||
|
|
||||||
from pydantic import (
|
|
||||||
BaseModel,
|
|
||||||
ValidationError, ValidationInfo,
|
|
||||||
ValidatorFunctionWrapHandler,
|
|
||||||
WrapValidator, AfterValidator, BeforeValidator
|
|
||||||
)
|
|
||||||
|
|
||||||
from ums.utils.const import SHARE_PATH
|
|
||||||
from ums.utils.schema import ExtractionSchema
|
|
||||||
|
|
||||||
class RiddleInformation(BaseModel):
|
|
||||||
# ignore:
|
|
||||||
# /usr/local/lib/python3.12/dist-packages/pydantic/_internal/_fields.py:172:
|
|
||||||
# UserWarning: Field name "validate" in "RiddleStatus" shadows an attribute in parent
|
|
||||||
# "RiddleInformation"
|
|
||||||
warnings.filterwarnings('ignore', category=UserWarning, lineno=172, module="pydantic")
|
|
||||||
|
|
||||||
"""
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
prompt: str | ExtractionSchema | None = None
|
|
||||||
"""
|
|
||||||
An optional prompt giving more details to the extraction agent, e.g., selecting a type of extraction/ task to do with the data.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
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 contain an `RiddleSolution` afterwards.
|
|
||||||
"""
|
|
||||||
|
|
||||||
validate: RiddleSubStatus = RiddleSubStatus()
|
|
||||||
"""
|
|
||||||
The validation step, i.e., does the gatekeeper accept the solution(s) 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(s) at `AgentMessage.solution`
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _transform_to_list(value : Any) -> List[Any]:
|
|
||||||
# type check of items is done next by pydantic
|
|
||||||
return value if isinstance(value, list) else [value]
|
|
||||||
|
|
||||||
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: Annotated[List[RiddleSolution], BeforeValidator(_transform_to_list)] = []
|
|
||||||
"""
|
|
||||||
The solutions of the riddle (or empty list if no solutions available)
|
|
||||||
(When assigning a single object of `RiddleSolution` will be convert to list with this single object.)
|
|
||||||
"""
|
|
||||||
|
|
||||||
data: List[RiddleData] = []
|
|
||||||
"""
|
|
||||||
The data to get the solution from.
|
|
||||||
"""
|
|
||||||
|
|
||||||
status: RiddleStatus = RiddleStatus()
|
|
||||||
"""
|
|
||||||
The status of the riddle.
|
|
||||||
"""
|
|
||||||
|
|
||||||
contacts : int = 0
|
|
||||||
"""
|
|
||||||
A counter representing the number of contacts the management had with this message.
|
|
||||||
Each time the management processes the message, this counter is incremented by 1.
|
|
||||||
Using this counter the management is able to detect cycles and stop them.
|
|
||||||
"""
|
|
||||||
|
|
||||||
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
|
|
||||||
"""
|
|
@ -1,35 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
ARG PIP_REQ_FILE
|
|
||||||
|
|
||||||
USER root
|
|
||||||
RUN mkdir -p /ums-agenten/plattform/ && mkdir -p /ums-agenten/persist/
|
|
||||||
|
|
||||||
COPY ./utils/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 ./utils/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"]
|
|
@ -1,16 +0,0 @@
|
|||||||
# 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:
|
|
||||||
# ...
|
|
@ -1,15 +0,0 @@
|
|||||||
# 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
|
|
@ -1,20 +0,0 @@
|
|||||||
{# 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 %}
|
|
@ -1,17 +0,0 @@
|
|||||||
# 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
|
|
||||||
tqdm
|
|
||||||
pdoc
|
|
||||||
fastapi
|
|
||||||
uvicorn[standard]
|
|
||||||
python-multipart
|
|
@ -1,19 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name='ums',
|
|
||||||
packages=find_packages(),
|
|
||||||
version='0.0.0',
|
|
||||||
description='UMS-Agenten',
|
|
||||||
author='Magnus Bender',
|
|
||||||
)
|
|
11
vars.sh
11
vars.sh
@ -1,18 +1,7 @@
|
|||||||
#/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"
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="refresh" content="0; url=./ums.html"/>
|
|
||||||
</head>
|
|
||||||
</html>
|
|
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
8
web/public/index.html
Normal file
8
web/public/index.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>UMS-Agenten Management</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Empty</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
7
web/public/static/bootstrap.bundle.min.js
vendored
7
web/public/static/bootstrap.bundle.min.js
vendored
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
6
web/public/static/bootstrap.min.css
vendored
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
2
web/public/static/jquery.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,16 +0,0 @@
|
|||||||
/** 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;
|
|
||||||
}
|
|
@ -1,126 +0,0 @@
|
|||||||
/** 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())
|
|
||||||
);
|
|
@ -1,65 +0,0 @@
|
|||||||
/** 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() )
|
|
||||||
);
|
|
@ -1,49 +0,0 @@
|
|||||||
{#-
|
|
||||||
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}} – 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}} – MGMT</h1>
|
|
||||||
|
|
||||||
{% block maincontent %}
|
|
||||||
{% endblock %}
|
|
||||||
</div>
|
|
||||||
{% block morefoot %}
|
|
||||||
{% endblock %}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,20 +0,0 @@
|
|||||||
{#-
|
|
||||||
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">↗ Web App: Table</a></li>
|
|
||||||
<li><a href="/app/new" target="_blank">↗ Web App: New Riddle</a></li>
|
|
||||||
<li><a href="/share/" target="_blank">↗ Data: Access the riddle files</a></li>
|
|
||||||
<li><a href="/api" target="_blank">↗ Documentation: API </a></li>
|
|
||||||
<li><a href="/docs/" target="_blank">↗ Documentation: Code </a></li>
|
|
||||||
</ul>
|
|
||||||
{% endblock %}
|
|
@ -1,21 +0,0 @@
|
|||||||
{#-
|
|
||||||
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>
|
|
@ -1,129 +0,0 @@
|
|||||||
{#-
|
|
||||||
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">← 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 %}
|
|
@ -1,144 +0,0 @@
|
|||||||
{#-
|
|
||||||
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">→ 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 %}
|
|
Loading…
x
Reference in New Issue
Block a user