12 Commits

Author SHA1 Message Date
a4d0803d20 Basic Table
All checks were successful
Build and push Docker image at git tag / build (push) Successful in 1m44s
2024-10-08 21:00:14 +02:00
e376956def Fix DB && Logs to Stdout in Docker
All checks were successful
Build and push Docker image at git tag / build (push) Successful in 1m54s
2024-10-08 14:11:57 +02:00
afc35be35a Better docs
All checks were successful
Build and push Docker image at git tag / build (push) Successful in 7m7s
2024-10-08 13:20:04 +02:00
28eee676c4 Begin Interface 2024-10-08 13:08:17 +02:00
cff39d61de DB works 2024-10-08 12:44:35 +02:00
d36ebf9694 Begin DB 2024-10-07 19:48:00 +02:00
b889b581f2 Messages via Types with Validation etc. 2024-10-06 15:09:55 +02:00
4ea424e0d1 Begin Types for Messages 2024-10-05 15:07:50 +02:00
9d0cd7e89b Devmode 2024-10-04 22:43:27 +02:00
00633347f4 Persists Path
All checks were successful
Build and push Docker image at git tag / build (push) Successful in 10m34s
2024-10-04 22:20:32 +02:00
ee01751170 Enable push
All checks were successful
Build and push Docker image at git tag / build (push) Successful in 2m26s
2024-10-04 21:37:35 +02:00
80d9d90aaa Begin Management in Docker
All checks were successful
Build and push Docker image at git tag / build (push) Successful in 26m48s
2024-10-04 21:34:09 +02:00
55 changed files with 6543 additions and 326 deletions

View File

@ -0,0 +1,38 @@
name: Build and push Docker image at git tag
on:
push:
tags:
- '*'
env:
IMAGE_REGISTRY: git.chai.uni-hamburg.de
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Get repository code
uses: actions/checkout@v4
- name: ARM64 add QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- name: ARM64 setup Docker buildx
uses: docker/setup-buildx-action@v3
- name: Build the Management
run: bash ./build-mgmt.sh -no-updates
#- name: Build the Agent
# run: bash ./build-agent.sh -no-updates
- name: Docker login
uses: docker/login-action@v3
with:
registry: ${{ env.IMAGE_REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Push the images
run: bash ./push-images.sh

8
.gitignore vendored Normal file
View File

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

View File

@ -1,3 +0,0 @@
FROM git.chai.uni-hamburg.de/ums-agenten/base-process:cpu
RUN apt install my-pkg

View File

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

View File

@ -1,132 +0,0 @@
### Ablauf
1. Daten und Rästel eingeben
- an Management von Benutzer (oder als Subrätsel)
```json
{
"id" : "xyz",
"riddle" : {
"context" : "The values of the variables of the following math problem are given in the text document as written numbers.",
"question" : "What is x+y?",
"solution_before" : "", # if trial > 0
"review_before": "" # if trial > 0
},
"trial" : 0, # did we try this before
"steps" : {
"extract" : true,
"solve" : true,
"validate": true,
},
"data" : [
{
"type" : "text",
"file" : "./my-test.txt"
},
...
]
}
```
2. Daten verteilen (`if "extract" : true`)
- an alle "🤖 Daten einlesen" vom Management
```json
{
"id" : "xyz",
"data" : [
{
"type" : "text",
"file" : "./my-test.txt"
},
...
]
}
```
3. Verarbeitete Daten annehmen (`if "extract" : true|false`)
- von allen "🤖 Daten einlesen" an Management
```json
{
"id" : "xyz",
"data" : [
{
"type" : "text",
"file" : "./my-test.txt",
"extraction" : "./my-test.json" # schema für alle gleich
},
...
]
}
```
4. Rätsel lösen (`if "solve" : true|false`)
- an (alle) "🤖 Lösung bestimmen" von Management
```json
{
"id" : "xyz",
"riddle" : { ... },
"data" : [
{
"type" : "text",
"file" : "./my-test.txt",
"extraction" : "./my-test.json"
},
...
]
}
```
- Muss ein "Subrätsel" erzeugen (können), wenn weitere Datenanalyse notwenig ist
- Mit einer Submenge der Daten bzw. konkreten Fragen etc.
5. Lösung eines Rätsel annehmen (`if "solve" : true`)
- von (alle) "🤖 Lösung bestimmen" an Management
```json
{
"id" : "xyz",
"solution" : "26",
"explanation" : "x = 12, y = 14, ...",
"sub_riddles" : [ # optional
"xyz-1",
"xyz-2"
],
"used_data" : [ # optional
{
"type" : "text",
"file" : "./my-test.txt",
"extraction" : "./my-test.json"
},
...
]
}
```
6. Lösung validieren lassen (`if "validate": true`)
- an (alle) "🤖 Lösung validieren" durch Management
```json
{
"id" : "xyz",
"riddle" : { ... }
"solution" : "26",
"explanation" : "x = 12, y = 14, ...",
"sub_riddles" : [ # optional
"xyz-1",
"xyz-2"
],
"used_data" : [ # optional
{
"type" : "text",
"file" : "./my-test.txt",
"extraction" : "./my-test.json"
},
...
]
}
```
7. Validierung annehmen (`if "validate": true`)
- von (alle) "🤖 Lösung validieren" an Management
```json
{
"id" : "xyz",
"solution" : "26",
"explanation" : "x = 12, y = 14, ...",
"accept" : false # or true
"review" : "y does not match Y"
}
```
8. Ausgabe oder Wiedervorlage
- von Management an User oder als neues Riddle
- `if "accept" : false`: Neues Riddle mit `trial+1`, `solution_before`, `review_before`
- `if "accept" : true`: User bekommt `solution, explanation, review`

46
build-mgmt.sh Executable file
View File

@ -0,0 +1,46 @@
#/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
docker build \
--pull \
--platform "linux/$platform" \
--file "$SCRIPTPATH/docker-mgmt/Dockerfile" \
--build-arg H_UID=1050 \
--build-arg PIP_REQ_FILE="$requirements" \
--build-arg H_GID=1050 \
--tag "$IMAGE_REGISTRY/$IMAGE_OWNER/$IMAGE_NAME_MGMT:$platform" \
"$SCRIPTPATH"
fi;
done;
if [ "$requirements" == "requirements.txt" ]; then
# extract requirements-frozen.txt
cid=$(docker create "$IMAGE_REGISTRY/$IMAGE_OWNER/$IMAGE_NAME_MGMT:arm64")
docker cp "$cid:/ums-agenten/requirements.txt" "$SCRIPTPATH/docker-mgmt/requirements-frozen.txt"
docker rm "$cid"
fi;

15
docker-agent/Dockerfile Normal file
View File

@ -0,0 +1,15 @@
# Agenten Plattform
#
# (c) 2024 Magnus Bender
# Institute of Humanities-Centered Artificial Intelligence (CHAI)
# Universitaet Hamburg
# https://www.chai.uni-hamburg.de/~bender
#
# source code released under the terms of GNU Public License Version 3
# https://www.gnu.org/licenses/gpl-3.0.txt
ARG FROM_IMAGE=
FROM $FROM_IMAGE

View File

@ -1,21 +1,33 @@
# 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:latest
image: git.chai.uni-hamburg.de/ums-agenten/management:arm64
#image: git.chai.uni-hamburg.de/ums-agenten/management:amd64
ports:
-
- 8000:80
environment:
- AGENTS_PROCESS=http://agent_process_1:3001,http://agent_process_2:3001
- AGENTS_SOLVE=http://agent_solve_1:3001
- AGENTS_GATEKEEPER=http://agent_gatekeeper_1:3001
volumes:
- /xxx/:
- ./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_process_1:
image: git.chai.uni-hamburg.de/ums-agenten/base-process:cpu
#image: git.chai.uni-hamburg.de/ums-agenten/base-process:gpu
build: .
environment:
- MANAGEMENT=http://management:3001
volumes:
- /xxx/:/ums-agenten/shared/

60
docker-mgmt/Dockerfile Normal file
View File

@ -0,0 +1,60 @@
# 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
ARG H_GID
ARG H_UID
ARG PIP_REQ_FILE
RUN apt update && \
apt install -y bash \
build-essential \
git \
curl \
ca-certificates \
python3-dev \
python3-pip
RUN DEBIAN_FRONTEND=noninteractive TZ=Europe/Berlin apt-get install -y tzdata \
&& cp /usr/share/zoneinfo/Europe/Berlin /etc/localtime \
&& echo "Europe/Berlin" > /etc/timezone
RUN apt-get install -y vim htop \
nginx supervisor \
&& rm -rf /var/lib/apt/lists
# sytem and user setup
RUN ln -s /usr/bin/python3 /usr/local/bin/python \
&& addgroup --gid $H_GID user \
&& adduser user --uid $H_UID --ingroup user --gecos "" --home /home/user/ --disabled-password
RUN mkdir -p /ums-agenten/plattform/ && mkdir -p /ums-agenten/persist/
COPY ./docker-mgmt/$PIP_REQ_FILE /ums-agenten/requirements.txt
RUN pip3 install --break-system-packages --no-cache-dir -r /ums-agenten/requirements.txt \
&& pip3 freeze > /ums-agenten/requirements.txt
# nginx settings and startup
COPY ./docker-mgmt/supervisor.conf /etc/supervisor/supervisord.conf
COPY ./docker-mgmt/nginx.conf /etc/nginx/nginx.conf
COPY ./docker-mgmt/app.conf /etc/nginx/sites-enabled/default
# install the code of the repo
COPY ./docker-mgmt/setup.py /ums-agenten/plattform/
RUN pip3 install --break-system-packages -e /ums-agenten/plattform/
WORKDIR /ums-agenten/plattform/ums/
COPY --chown=user:user ./ums/ /ums-agenten/plattform/ums/
COPY --chown=user:user ./web/ /ums-agenten/plattform/web/
# run nginx and uvicorn
ENTRYPOINT ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]

45
docker-mgmt/app.conf Normal file
View File

@ -0,0 +1,45 @@
# Agenten Plattform
#
# (c) 2024 Magnus Bender
# Institute of Humanities-Centered Artificial Intelligence (CHAI)
# Universitaet Hamburg
# https://www.chai.uni-hamburg.de/~bender
#
# source code released under the terms of GNU Public License Version 3
# https://www.gnu.org/licenses/gpl-3.0.txt
server {
listen 80 default_server;
server_name _;
root /ums-agenten/plattform/web/public/;
index index.html;
location = / {
server_name_in_redirect off;
port_in_redirect off;
absolute_redirect off;
return 303 /index;
}
location / {
try_files $uri $uri/ @dynamic;
}
location @dynamic {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off;
proxy_buffering off;
proxy_pass http://unix:/tmp/uvicorn.sock;
}
}

57
docker-mgmt/nginx.conf Normal file
View File

@ -0,0 +1,57 @@
# 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;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
}
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# SSL Settings
##
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
##
# Logging Settings
##
access_log off;
error_log /var/log/nginx/error.log;
##
# Gzip Settings
##
gzip on;
##
# Virtual Host Configs
##
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

View File

@ -0,0 +1,31 @@
annotated-types==0.7.0
anyio==4.6.0
certifi==2024.8.30
charset-normalizer==3.3.2
click==8.1.7
fastapi==0.115.0
h11==0.14.0
httptools==0.6.1
idna==3.10
Jinja2==3.1.4
MarkupSafe==2.1.5
pdoc==14.7.0
pydantic==2.9.2
pydantic_core==2.23.4
Pygments==2.18.0
python-dotenv==1.0.1
python-multipart==0.0.12
PyYAML==6.0.2
requests==2.32.3
setuptools==68.1.2
sniffio==1.3.1
starlette==0.38.6
supervisor==4.2.5
tqdm==4.66.5
typing_extensions==4.12.2
urllib3==2.2.3
uvicorn==0.31.0
uvloop==0.20.0
watchfiles==0.24.0
websockets==13.1
wheel==0.42.0

View File

@ -0,0 +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
# non frozen dependecies, to use latest
requests
tqdm
pdoc
fastapi
uvicorn[standard]
python-multipart

19
docker-mgmt/setup.py Normal file
View File

@ -0,0 +1,19 @@
# Agenten Plattform
#
# (c) 2024 Magnus Bender
# Institute of Humanities-Centered Artificial Intelligence (CHAI)
# Universitaet Hamburg
# https://www.chai.uni-hamburg.de/~bender
#
# source code released under the terms of GNU Public License Version 3
# https://www.gnu.org/licenses/gpl-3.0.txt
from setuptools import find_packages, setup
setup(
name='ums',
packages=find_packages(),
version='0.0.0',
description='UMS-Agenten',
author='Magnus Bender',
)

View File

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

17
docs.sh Executable file
View File

@ -0,0 +1,17 @@
#!/bin/bash
# Agenten Plattform
#
# (c) 2024 Magnus Bender
# Institute of Humanities-Centered Artificial Intelligence (CHAI)
# Universitaet Hamburg
# https://www.chai.uni-hamburg.de/~bender
#
# source code released under the terms of GNU Public License Version 3
# https://www.gnu.org/licenses/gpl-3.0.txt
pdoc ./ums/ \
--output-directory ./web/public/docs/ \
--no-browser \
--docformat google \
--template-directory ./utils/doc-template

44
push-images.sh Executable file
View File

@ -0,0 +1,44 @@
#/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"
day_tag=$(date '+%Y-%m-%d')
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}}')
echo "$images_a\n$images_b" | while read image_url ;
do
image_name="${image_url##*/}"
image_name="${image_name%%:*}"
image_tag="${image_url##*:}"
if [[ "$image_tag" =~ ^((gpu)|(cpu)-)?((arm64)|(amd64))$ ]];
then
echo "Push:"
echo " $IMAGE_REGISTRY/$IMAGE_OWNER/$image_name:$image_tag"
echo " $IMAGE_REGISTRY/$IMAGE_OWNER/$image_name:$image_tag-$day_tag"
docker push "$IMAGE_REGISTRY/$IMAGE_OWNER/$image_name:$image_tag"
docker tag "$IMAGE_REGISTRY/$IMAGE_OWNER/$image_name:$image_tag" \
"$IMAGE_REGISTRY/$IMAGE_OWNER/$image_name:$image_tag-$day_tag"
docker push "$IMAGE_REGISTRY/$IMAGE_OWNER/$image_name:$image_tag-$day_tag"
fi;
done

View File

@ -1,57 +0,0 @@
from abc import abstractmethod
from typing import List, Dict
from o_types import *
class Agent():
def process_result():
pass
# hier die Kommunikation mit dem Management Server
""" ===== """
class ProcessAgent(Agent):
TYPE:str = None
@abstractmethod
def on_process_data(self, data:List[Data]):
pass
class TextAgentExample(ProcessAgent):
TYPE = Data.TYPE_TEXT
def on_process_data(self, data:List[Data]):
# Studi Code
self.process_result(Data(...))
class ImageAgentExample(ProcessAgent):
TYPE = Data.TYPE_IMAGE
class AudioAgentExample(ProcessAgent):
TYPE = Data.TYPE_AUDIO
""" ===== """
class SolveAgent(Agent):
@abstractmethod
def on_riddle_data(self, riddle:Riddle, data:List[Data]):
pass
# Studi Code
self.new_riddle(Riddle('x', 'y', 'z'))
self.send_riddle_result(...)
""" ===== """
class GatekeeperAgent(Agent):
@abstractmethod
def on_result_check(self, xyz):
# Studi Code
self.send_check_result(accept=True|False, ...)

View File

@ -1,80 +0,0 @@
from agent import Agent
class ProcessTextAgent(Agent):
TYPE = "text" or "image"
def on_process_data(self, data:List[Dict]):
return filter(lambda x: x["type"] == self.TYPE, data)
# {type: "text", "file" : ""}
class ExampleAgent(ProcessTextAgent):
def on_process_data(self, data:List[Dict]):
# {type: "text", "file" : ""}
self.after_process_data(result)
# disk -> 123
# agent 1 disk -> 123
class Agent:
def request_management(self, request):
request.get(os.gentenv('MANAGEMENT'), data)
# https ...
def write_result(...):
self.request_management.write(...)
def read_result(...):
self.request_management.read(...)
class ProcessAgent(Agent):
TYPE : str = None
def on_process_data(self, data:List[Dict]):
pass
class TextAgent(ProcessAgent):
TYPE = 'text'
text_data = self.on_process_data()
def on_process_data(self, data:List[Dict]):
text_data = filter_text_data()
processed_data = text_func1(text_data)
self.write_result(processed_data)
def text_func1():
...
def text_func2():
...
def text_func3():
...
class ImageAgent(ProcessAgent):
TYPE = 'image'
image_data = self.on_process_data()
def on_process_data(self, data:list[Dict])
image_data = filter_image_data()
def image_func1():
...
...

View File

@ -1,43 +0,0 @@
from typing import List
class Riddle():
def __init__(self):
self.id = "" # -> management.get_riddle_by_id(id)
self.parent_id = "" # ?!
self.context = "1234"
self.question = "12"
self.data:List[Data] = []
def from_json(json):
return Riddle(json)
def to_json(self):
return {
"context" : self.context,
"question" : self.question,
"solution_before" : "", # if trial > 0
"review_before": "" # if trial > 0
}
class Data():
TYPE_TEXT = "text"
TYPE_AUDIO = "audio"
TYPE_IMAGE = "image"
def __init__(self):
self.type = 1
self.file = 2
self.extraction = 3
def to_json(self):
return {
"type" : self.type,
"file" : self.file,
"extraction" : self.extraction
}

9
ums/__cli__.py Normal file
View File

@ -0,0 +1,9 @@
# Agenten Plattform
#
# (c) 2024 Magnus Bender
# Institute of Humanities-Centered Artificial Intelligence (CHAI)
# Universitaet Hamburg
# https://www.chai.uni-hamburg.de/~bender
#
# source code released under the terms of GNU Public License Version 3
# https://www.gnu.org/licenses/gpl-3.0.txt

9
ums/__init__.py Normal file
View File

@ -0,0 +1,9 @@
# Agenten Plattform
#
# (c) 2024 Magnus Bender
# Institute of Humanities-Centered Artificial Intelligence (CHAI)
# Universitaet Hamburg
# https://www.chai.uni-hamburg.de/~bender
#
# source code released under the terms of GNU Public License Version 3
# https://www.gnu.org/licenses/gpl-3.0.txt

9
ums/agent/__init__.py Normal file
View File

@ -0,0 +1,9 @@
# Agenten Plattform
#
# (c) 2024 Magnus Bender
# Institute of Humanities-Centered Artificial Intelligence (CHAI)
# Universitaet Hamburg
# https://www.chai.uni-hamburg.de/~bender
#
# source code released under the terms of GNU Public License Version 3
# https://www.gnu.org/licenses/gpl-3.0.txt

View File

@ -0,0 +1,9 @@
# Agenten Plattform
#
# (c) 2024 Magnus Bender
# Institute of Humanities-Centered Artificial Intelligence (CHAI)
# Universitaet Hamburg
# https://www.chai.uni-hamburg.de/~bender
#
# source code released under the terms of GNU Public License Version 3
# https://www.gnu.org/licenses/gpl-3.0.txt

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

@ -0,0 +1,181 @@
# 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, BaseModel
from ums.utils import PERSIST_PATH, AgentMessage
class RowObject(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).
"""
class DB():
_DB_TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
def __init__(self):
self.db = sqlite3.connect(
os.path.join(PERSIST_PATH, 'messages.db'),
check_same_thread=False
)
self.db.row_factory = sqlite3.Row
atexit.register(lambda db : db.close(), self.db)
self.db_lock = Lock()
self._assure_tables()
def _assure_tables(self):
self.db_lock.acquire()
with self.db:
self.db.execute("""CREATE TABLE IF NOT EXISTS Messages (
count INTEGER PRIMARY KEY AUTOINCREMENT,
id TEXT,
sender TEXT,
recipient TEXT,
time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
json BLOB,
processed BOOL DEFAULT FALSE
)""")
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()
def __iter__(self) -> Generator[RowObject, 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,
time_after:int|None=None, time_before:int|None=None,
limit:int=20, offset:int=0
) -> Generator[RowObject, None, None]:
where = []
params = {
"lim": limit,
"off": offset
}
for v,n in ((id,'id'), (sender,'sender'), (recipient,'recipient'), (processed,'processed')):
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:
for row in self.db.execute(
"SELECT * FROM Messages {} ORDER BY time DESC LIMIT :lim OFFSET :off".format(where_clause),
params
):
yield self._create_row_object(row)
def _create_row_object(self, row:sqlite3.Row) -> RowObject:
return RowObject(
count=row['count'],
sender=row['sender'],
recipient=row['recipient'],
time=int(datetime.strptime(row['time'], self._DB_TIME_FORMAT).timestamp()),
message=AgentMessage.model_validate_json(row['json']),
processed=row['processed']
)
def by_count(self, count:int) -> RowObject|None:
with self.db:
try:
return self._create_row_object(
self.db.execute("SELECT * FROM Messages WHERE count = ?", (count,)).fetchone()
)
except:
return None

View File

@ -0,0 +1,64 @@
# 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 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
class Interface():
_PREFIX = "/app"
def __init__(self, template:Jinja2Templates, db:DB):
self.template = template
self.db = db
self.router = APIRouter(
prefix=self._PREFIX,
tags=["app, gui"]
)
self._add_routes()
def _add_routes(self):
@self.router.get("/", response_class=RedirectResponse, summary="Redirect")
def index(request: Request) -> RedirectResponse:
return RedirectResponse(self._PREFIX + "/table")
@self.router.get("/table", response_class=HTMLResponse, summary="Table of messages")
def table(request: Request, limit:int=10, offset:int=0):
db_args = {
"limit" : limit,
"offset" : offset
}
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("/new", response_class=HTMLResponse, summary="Add new riddle")
def new(request: Request):
return self.template.TemplateResponse(
'new.html',
{"request" : request}
)

95
ums/management/main.py Normal file
View File

@ -0,0 +1,95 @@
# Agenten Plattform
#
# (c) 2024 Magnus Bender
# Institute of Humanities-Centered Artificial Intelligence (CHAI)
# Universitaet Hamburg
# https://www.chai.uni-hamburg.de/~bender
#
# source code released under the terms of GNU Public License Version 3
# https://www.gnu.org/licenses/gpl-3.0.txt
import os
from datetime import datetime
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from ums.management.interface import Interface
from ums.management.db import DB
from ums.utils import AgentMessage, RiddleData, RiddleDataType, RiddleSolution, TEMPLATE_PATH
class WebMain():
def __init__(self):
self._init_app()
self._init_templates()
self.db = DB()
self._add_routes()
self._add_routers()
def _init_app(self):
self.app = FastAPI(
title="Agenten Plattform",
description="Agenten Plattform Management",
openapi_url="/api/schema.json",
docs_url='/api',
redoc_url=None
)
def _init_templates(self):
self.template = Jinja2Templates(
directory=TEMPLATE_PATH,
auto_reload=True
)
self.template.env.globals["timestamp2date"] = lambda t: \
datetime.fromtimestamp(t).strftime("%H:%M:%S %d.%m.%Y")
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.get("/test", summary="Test")
def huhu(request: Request) -> AgentMessage:
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
ex.solution = RiddleSolution(
solution="Otto",
explanation="Written in line 6 after 'Name:'"
)
ins_count = self.db.add_message('from', 'to', ex)
self.db.set_processed(ins_count)
return ex
if __name__ == "ums.management.main" and os.environ.get('SERVE', 'false') == 'true':
main = WebMain()
app = main.app

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

@ -0,0 +1,21 @@
# Agenten Plattform
#
# (c) 2024 Magnus Bender
# Institute of Humanities-Centered Artificial Intelligence (CHAI)
# Universitaet Hamburg
# https://www.chai.uni-hamburg.de/~bender
#
# source code released under the terms of GNU Public License Version 3
# https://www.gnu.org/licenses/gpl-3.0.txt
from ums.utils.types import (
RiddleInformation,
AgentMessage,
Riddle,
RiddleSolution,
RiddleData,
RiddleDataType,
RiddleStatus
)
from ums.utils.const import *

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

@ -0,0 +1,21 @@
# Agenten Plattform
#
# (c) 2024 Magnus Bender
# Institute of Humanities-Centered Artificial Intelligence (CHAI)
# Universitaet Hamburg
# https://www.chai.uni-hamburg.de/~bender
#
# source code released under the terms of GNU Public License Version 3
# https://www.gnu.org/licenses/gpl-3.0.txt
"""
This file contains shared constants.
See the content ...
"""
import os
BASE_PATH = '/ums-agenten'
SHARE_PATH = os.path.join(BASE_PATH, 'share')
PERSIST_PATH = os.path.join(BASE_PATH, 'persist')
TEMPLATE_PATH = os.path.join(BASE_PATH, 'plattform', 'web', 'templates')

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

@ -0,0 +1,292 @@
# Agenten Plattform
#
# (c) 2024 Magnus Bender
# Institute of Humanities-Centered Artificial Intelligence (CHAI)
# Universitaet Hamburg
# https://www.chai.uni-hamburg.de/~bender
#
# source code released under the terms of GNU Public License Version 3
# https://www.gnu.org/licenses/gpl-3.0.txt
"""
This represents the basic types used to interact with the management.
The types are implemented using [pydantic](https://docs.pydantic.dev/).
It provides validation, allow JSON serialization and works well with [FastAPI](https://fastapi.tiangolo.com/) which is used internally for the http request between the agents and the management.
### Example
```python
ex = AgentMessage(
id="ex1",
riddle={
"context":"Example 1",
"question":"Get the name of the person."
},
data=[
RiddleData(
type=RiddleDataType.TEXT,
file_plain="./cv.txt"
)
]
)
ex.status.extract.required = False
```
```json
{
"id": "ex1",
"sub_ids": [],
"riddle": {
"context": "Example 1",
"question": "Get the name of the person.",
"solutions_before": []
},
"solution": null,
"data": [
{
"type": "text",
"file_plain": "/ums-agenten/share/cv.txt",
"file_extracted": null
}
],
"status": {
"extract": {
"required": false,
"finished": false
},
"solve": {
"required": true,
"finished": false
},
"validate": {
"required": true,
"finished": false
},
"trial": 0,
"solved": false
}
}
```
```python
ex.solution = RiddleSolution(
solution="Otto",
explanation="Written in line 6 after 'Name:'"
)
```
```json
{
...
"solution": {
"solution": "Otto",
"explanation": "Written in line 6 after 'Name:'",
"used_data": [],
"accepted": false,
"review": null
},
...
}
```
"""
import os
from enum import Enum
from typing import List
from typing_extensions import Annotated
from pydantic import BaseModel
from pydantic.functional_validators import AfterValidator
from ums.utils.const import SHARE_PATH
class RiddleInformation(BaseModel):
"""
This is the basic class used as superclass for all message and infos
about a riddle.
"""
class RiddleDataType(Enum):
"""
Enum for the three types of data used in a riddle.
"""
TEXT = "text"
IMAGE = "image"
AUDIO = "audio"
def _check_data_file(file_name:str) -> str:
if not file_name.startswith('/'):
file_name = os.path.join(SHARE_PATH, file_name)
assert file_name.startswith(SHARE_PATH), "The data file needs to be in {}!".format(SHARE_PATH)
file_name = os.path.realpath(file_name, strict=False)
assert os.path.isfile(file_name), "The data file {} does not exist!".format(file_name)
return file_name
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)]
"""
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)] | None = None
"""
The processed files (as path to file system), i.e., a schematic file containing all extracted informations.
The path will be validated and must start with `SHARE_PATH` (or be relative to `SHARE_PATH`).
The file must exist.
"""
class RiddleSolution(RiddleInformation):
"""
A solution of a riddle.
"""
solution: str
"""
The textual value of the solution.
"""
explanation: str
"""
An explanation of the solution.
"""
used_data: List[RiddleData] = []
"""
The data items used to create the solution (optional).
"""
accepted : bool = False
"""
If the solution is accepted by validator/ gatekeeper.
"""
review: str | None = None
"""
A review of the solution (if None: not tried to validate)
"""
class Riddle(RiddleInformation):
"""
The riddle (the task description and possibly a solution)
"""
context: str
"""
The context of the riddle (as textual string).
"""
question: str
"""
The actual main question of the riddle (as textual string).
"""
solutions_before: List[RiddleSolution] = []
"""
If already tried to solve this riddle before, the (not accepted) solutions are stored here
"""
class RiddleSubStatus(RiddleInformation):
"""
The sub status for each possible step a riddle may go though.
"""
required: bool = True
"""
Is this step required (i.e., requested)
"""
finished: bool = False
"""
Was this step already executed.
"""
class RiddleStatus(RiddleInformation):
"""
The status of a riddle, will be mostly changed by Management when the riddle is sent to different agents while solving it.
"""
extract: RiddleSubStatus = RiddleSubStatus()
"""
The first extract step (image, text, audio -> more sematic data)
The `RiddleData` items in `AgentMessage.data` shall have `file_extracted` afterwards.
"""
solve: RiddleSubStatus = RiddleSubStatus()
"""
The *main* solving step.
`AgentMessage.solution` shall be an `RiddleSolution` afterwards.
"""
validate: RiddleSubStatus = RiddleSubStatus()
"""
The validation step, i.e., does the gatekeeper accept the solution in `AgentMessage.solution`.
"""
trial: int = 0
"""
A counter for the number of trials.
Each time the gatekeeper does not accept a solution of this riddle, the value is incremented.
"""
solved: bool = False
"""
True, after the gatekeeper accepts the solution at `AgentMessage.solution`
"""
class AgentMessage(RiddleInformation):
"""
The basic message, which is sent be the agent and the management.
The objects will be JSON en- and decoded.
"""
id: str
"""
The riddle id, e.g., ``ex1``
This is a unique string and identifies the riddle.
"""
sub_ids: List[str] = []
"""
There might be cases, when an agent decided to split in riddle in multiple *smaller* steps.
Each *sub* riddle will then get its own id (i.e., ``ex1-sub1``) while the sub id is added here as reference.
"""
riddle: Riddle
"""
The riddle to solve.
"""
solution: RiddleSolution | None = None
"""
The solution of the riddle (or empty if no solution available)
"""
data: List[RiddleData] = []
"""
The data to get the solution from.
"""
status: RiddleStatus = RiddleStatus()
"""
The status of the riddle.
"""

View File

@ -0,0 +1,20 @@
{# Agenten Plattform
#
# (c) 2024 Magnus Bender
# Institute of Humanities-Centered Artificial Intelligence (CHAI)
# Universitaet Hamburg
# https://www.chai.uni-hamburg.de/~bender
#
# source code released under the terms of GNU Public License Version 3
# https://www.gnu.org/licenses/gpl-3.0.txt
#}
{% extends "default/module.html.jinja2" %}
{% macro is_public(doc) %}
{% if doc.name in ("model_config", "model_fields", "model_computed_fields") %}
{% else %}
{{ default_is_public(doc) }}
{% endif %}
{% endmacro %}

17
vars.sh Executable file
View File

@ -0,0 +1,17 @@
#/bin/bash
# Agenten Plattform
#
# (c) 2024 Magnus Bender
# Institute of Humanities-Centered Artificial Intelligence (CHAI)
# Universitaet Hamburg
# https://www.chai.uni-hamburg.de/~bender
#
# source code released under the terms of GNU Public License Version 3
# https://www.gnu.org/licenses/gpl-3.0.txt
IMAGE_REGISTRY="git.chai.uni-hamburg.de"
IMAGE_OWNER="ums-agenten"
IMAGE_NAME_AGENT="base-agent"
IMAGE_NAME_MGMT="management"
PLATFORMS="amd64 arm64 gpu"

View File

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

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

View File

View File

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

@ -0,0 +1,48 @@
{#-
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>
<script src="/static/main.js"></script>
<title>{{title}} &ndash; MGMT</title>
<!--
Agenten Plattform
(c) 2024 Magnus Bender
Institute of Humanities-Centered Artificial Intelligence (CHAI)
Universitaet Hamburg
https://www.chai.uni-hamburg.de/~bender
source code released under the terms of GNU Public License Version 3
https://www.gnu.org/licenses/gpl-3.0.txt
-->
{% block morehead %}
{% endblock %}
</head>
<body>
<div class="container">
<h1>{{title}} &ndash; MGMT</h1>
{% block maincontent %}
{% endblock %}
</div>
</body>
</html>

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

@ -0,0 +1,19 @@
{#-
Agenten Plattform
(c) 2024 Magnus Bender
Institute of Humanities-Centered Artificial Intelligence (CHAI)
Universitaet Hamburg
https://www.chai.uni-hamburg.de/~bender
source code released under the terms of GNU Public License Version 3
https://www.gnu.org/licenses/gpl-3.0.txt
-#}
{% extends "base.html" %}
{% set title = "Overview" %}
{% block maincontent %}
<ul>
<li><a href="/app/table" target="_blank">&nearr; Web App: Table</a></li>
<li><a href="/app/new" target="_blank">&nearr; Web App: New Riddle</a></li>
<li><a href="/api/" target="_blank">&nearr; Documentation: API </a></li>
<li><a href="/docs/" target="_blank">&nearr; Documentation: Code </a></li>
</ul>
{% endblock %}

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

@ -0,0 +1,19 @@
{#-
Agenten Plattform
(c) 2024 Magnus Bender
Institute of Humanities-Centered Artificial Intelligence (CHAI)
Universitaet Hamburg
https://www.chai.uni-hamburg.de/~bender
source code released under the terms of GNU Public License Version 3
https://www.gnu.org/licenses/gpl-3.0.txt
-#}
{% extends "base.html" %}
{% set title = "New" %}
{% block maincontent %}
<div class="float-end">
<a href="/app/table" class="btn btn-secondary">&larr; Back to Messages</a>
</div>
{% endblock %}

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

@ -0,0 +1,109 @@
{#-
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="float-end">
<a href="/app/new" class="btn btn-secondary">&rarr; Add a Riddle</a>
</div>
{{pagination()}}
<table class="table table-striped">
<thead>
{% for item in db.iterate(**db_args) %}
{% if loop.index == 1 %}
<tr id="row_0">
{% for field in item.__fields__.keys() %}
<th>
{% if field == 'time' %}
<input type="text" class="value_filter" name="filter_time_before" value="{{db_args.time_before}}" class="form-control" placeholder="Before">
<input type="text" class="value_filter" name="filter_time_after" value="{{db_args.time_after}}" class="form-control" placeholder="After">
{% elif field not in ('message', 'count') %}
<input type="text" class="value_filter" name="filter_{{field}}" value="{{db_args[field]}}" class="form-control" placeholder="Filter">
{% endif %}
{{ field.title() }}
</th>
{% endfor %}
</tr>
</thead><tbody>
{% endif %}
<tr id="row_{{loop.index}}">
{% for field in item.__fields__.keys() %}
{% if field == "message" %}
<td>
<button type="button" class="btn btn-outline-secondary btn-outline" data-bs-toggle="modal" data-bs-target="#row_message_{{loop.index}}">
Show Message
</button>
<div class="modal fade" id="row_message_{{loop.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>{{ item[field].model_dump_json(indent=2)|string }}</pre>
</div>
</div>
</div>
</div>
</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, ...!
</div>
{% endfor %}
</tbody>
</table>
<div class="float-end">
<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="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>
{{pagination()}}
{% endblock %}