Compare commits
3 Commits
66f9304cc6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
a56402c2cc
|
|||
|
f2015e2c71
|
|||
|
e43c534ceb
|
@@ -4,6 +4,7 @@
|
||||
- [Reeseapps vs Reeselink](#reeseapps-vs-reeselink)
|
||||
- [Reeselink Addresses](#reeselink-addresses)
|
||||
- [Reeseapps Addresses](#reeseapps-addresses)
|
||||
- [Converting Unifi Records to AWS Records](#converting-unifi-records-to-aws-records)
|
||||
|
||||
## Reeseapps vs Reeselink
|
||||
|
||||
@@ -28,3 +29,14 @@ aws route53 change-resource-record-sets --hosted-zone-id $(cat active/aws_route5
|
||||
```bash
|
||||
aws route53 change-resource-record-sets --hosted-zone-id $(cat active/aws_route53/secrets/reeseapps-zoneid) --change-batch file://active/aws_route53/secrets/reeseapps.json
|
||||
```
|
||||
|
||||
## Converting Unifi Records to AWS Records
|
||||
|
||||
The script `unifi_to_aws.py` will create a file at
|
||||
`secrets/unifi_reeselink_records.json` which contains all `reeselink.com`
|
||||
domains in the unifi server converted to AWS route53 batch format. Simply run
|
||||
the script and then use that file to update reeselink.com records.
|
||||
|
||||
```python
|
||||
python active/aws_route53/unifi_to_aws.py
|
||||
```
|
||||
|
||||
76
active/aws_route53/sync_unifi_records.sh
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/bin/bash
|
||||
|
||||
# --- Configuration ---
|
||||
PYTHON_SCRIPT="active/aws_route53/unifi_to_aws.py"
|
||||
ZONE_ID_FILE="active/aws_route53/secrets/reeselink-zoneid"
|
||||
RECORDS_FILE="active/aws_route53/secrets/unifi_reeselink_records.json"
|
||||
|
||||
# --- Colors for logging ---
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# --- Logging Function ---
|
||||
log() {
|
||||
echo -e "[$(date +'%Y-%m-%d %H:%M:%S')] $1"
|
||||
}
|
||||
|
||||
error_exit() {
|
||||
echo -e "[$(date +'%Y-%m-%d %H:%M:%S')] ${RED}ERROR: $1${NC}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- 1. Pre-flight Checks ---
|
||||
log "${YELLOW}Starting Route53 update process...${NC}"
|
||||
|
||||
if [[ ! -f "$PYTHON_SCRIPT" ]]; then
|
||||
error_exit "Python script not found at $PYTHON_SCRIPT"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$ZONE_ID_FILE" ]]; then
|
||||
error_exit "Zone ID file not found at $ZONE_ID_FILE"
|
||||
fi
|
||||
|
||||
# --- 2. Run Python Script ---
|
||||
log "Running $PYTHON_SCRIPT to generate JSON records..."
|
||||
|
||||
# Execute the python script
|
||||
python "$PYTHON_SCRIPT"
|
||||
|
||||
# Check the exit code of the python script
|
||||
if [[ $? -eq 0 ]]; then
|
||||
log "${GREEN}Python script executed successfully.${NC}"
|
||||
else
|
||||
error_exit "Python script failed. Aborting AWS update to prevent corrupting DNS."
|
||||
fi
|
||||
|
||||
# Verify the output file actually exists after the python run
|
||||
if [[ ! -f "$RECORDS_FILE" ]]; then
|
||||
error_exit "Python script reported success, but $RECORDS_FILE was not found."
|
||||
fi
|
||||
|
||||
# --- 3. Update Route53 ---
|
||||
# Read the Zone ID from the secret file
|
||||
ZONE_ID=$(cat "$ZONE_ID_FILE" | tr -d '\n\r ')
|
||||
|
||||
if [[ -z "$ZONE_ID" ]]; then
|
||||
error_exit "Zone ID file is empty or could not be read."
|
||||
fi
|
||||
|
||||
log "Updating Route53 records for Zone ID: $ZONE_ID..."
|
||||
|
||||
# Run the AWS CLI command
|
||||
# Using file:// prefix as required by AWS CLI for local files
|
||||
aws route53 change-resource-record-sets \
|
||||
--hosted-zone-id "$ZONE_ID" \
|
||||
--change-batch "file://$RECORDS_FILE"
|
||||
|
||||
# Check the exit code of the AWS command
|
||||
if [[ $? -eq 0 ]]; then
|
||||
log "${GREEN}Route53 records updated successfully!${NC}"
|
||||
else
|
||||
error_exit "AWS CLI command failed. Check your AWS credentials and JSON formatting."
|
||||
fi
|
||||
|
||||
log "${GREEN}Process complete.${NC}"
|
||||
113
active/aws_route53/unifi_to_aws.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import requests
|
||||
|
||||
# Configuration
|
||||
API_KEY = os.environ.get("API_KEY")
|
||||
URL_DEVICES = "https://10.1.0.1/proxy/network/v2/api/site/default/static-dns/devices"
|
||||
URL_POLICIES = "https://10.1.0.1/proxy/network/integration/v1/sites/88f7af54-98f8-306a-a1c7-c9349722b1f6/dns/policies"
|
||||
OUTPUT_FILE = "active/aws_route53/secrets/unifi_reeselink_records.json"
|
||||
|
||||
ALLOWED_DOMAIN = "reeselink.com"
|
||||
FIXED_TTL = 60
|
||||
|
||||
# Headers
|
||||
headers = {"Accept": "application/json", "X-API-Key": API_KEY}
|
||||
|
||||
|
||||
def fetch_json(url: str) -> Any:
|
||||
"""Helper to perform the GET request and return parsed JSON."""
|
||||
response = requests.get(
|
||||
url,
|
||||
headers=headers,
|
||||
verify=False, # -k: Don't verify SSL certificate
|
||||
allow_redirects=True, # -L: Follow redirects
|
||||
)
|
||||
if response.status_code != 200:
|
||||
print(f"Error: Received status code {response.status_code} from {url}")
|
||||
print(f"Response: {response.text}")
|
||||
sys.exit(1)
|
||||
return response.json()
|
||||
|
||||
|
||||
def main():
|
||||
all_changes: List[Dict[str, Any]] = []
|
||||
|
||||
# 1. Process Devices API
|
||||
devices_data = fetch_json(URL_DEVICES)
|
||||
devices_count = 0
|
||||
|
||||
# devices_data is expected to be a list: [{hostname: ..., ip_address: ...}, ...]
|
||||
for device in devices_data:
|
||||
hostname = device.get("hostname", "")
|
||||
ip = device.get("ip_address", "")
|
||||
|
||||
if hostname.endswith(ALLOWED_DOMAIN):
|
||||
all_changes.append(
|
||||
{
|
||||
"Action": "UPSERT",
|
||||
"ResourceRecordSet": {
|
||||
"Name": hostname,
|
||||
"Type": "A",
|
||||
"TTL": FIXED_TTL,
|
||||
"ResourceRecords": [{"Value": ip}],
|
||||
},
|
||||
}
|
||||
)
|
||||
devices_count += 1
|
||||
|
||||
# 2. Process Policies API
|
||||
policies_response = fetch_json(URL_POLICIES)
|
||||
policies_count = 0
|
||||
|
||||
# policies_response is expected to be a dict: {"data": [{domain: ..., ipv4Address: ...}, ...]}
|
||||
policies_list = policies_response.get("data", [])
|
||||
for policy in policies_list:
|
||||
domain = policy.get("domain", "")
|
||||
ip = policy.get("ipv4Address", "")
|
||||
|
||||
if domain.endswith(ALLOWED_DOMAIN):
|
||||
all_changes.append(
|
||||
{
|
||||
"Action": "UPSERT",
|
||||
"ResourceRecordSet": {
|
||||
"Name": domain,
|
||||
"Type": "A",
|
||||
"TTL": FIXED_TTL,
|
||||
"ResourceRecords": [{"Value": ip}],
|
||||
},
|
||||
}
|
||||
)
|
||||
policies_count += 1
|
||||
|
||||
# Construct Final AWS Payload
|
||||
final_payload = {
|
||||
"Comment": "Combined records from Unifi devices and policies",
|
||||
"Changes": all_changes,
|
||||
}
|
||||
|
||||
# Write to file
|
||||
try:
|
||||
# Ensure directory exists
|
||||
os.makedirs(os.path.dirname(OUTPUT_FILE), exist_ok=True)
|
||||
with open(OUTPUT_FILE, "w") as f:
|
||||
json.dump(final_payload, f, indent=4)
|
||||
except Exception as e:
|
||||
print(f"Error writing to file: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Print Summary
|
||||
print(f"Successfully processed records:")
|
||||
print(f" - devices: {devices_count}")
|
||||
print(f" - policies: {policies_count}")
|
||||
print(f"Total records in file: {len(all_changes)}")
|
||||
print(f"Saved to {OUTPUT_FILE}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Suppress InsecureRequestWarning for verify=False
|
||||
requests.packages.urllib3.disable_warnings() # type: ignore
|
||||
main()
|
||||
@@ -154,7 +154,7 @@ curl -L -X POST 'https://aipi.reeseapps.com/v1/chat/completions' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer sk-1234' \
|
||||
-d '{
|
||||
"model": "gpt-4o-mini", # 👈 REPLACE with 'public model name' for any db-model
|
||||
"model": "driveripper/think",
|
||||
"messages": [
|
||||
{
|
||||
"content": "Hey, how's it going",
|
||||
|
||||
@@ -70,11 +70,6 @@ active/container_caddy/install_caddy_proxy.yaml
|
||||
ansible-playbook \
|
||||
-i ansible/inventory.yaml \
|
||||
active/container_caddy/install_caddy_deskwork.yaml
|
||||
|
||||
# Toybox (AI) Proxy
|
||||
ansible-playbook \
|
||||
-i ansible/inventory.yaml \
|
||||
active/container_caddy/install_caddy_toybox.yaml
|
||||
```
|
||||
|
||||
See ansible playbook [install_caddy.yaml](/active/container_caddy/install_caddy.yaml)
|
||||
|
||||
@@ -58,7 +58,7 @@ Now you can install the DDNS service with something like:
|
||||
```bash
|
||||
ansible-playbook \
|
||||
-i ansible/inventory.yaml \
|
||||
-l proxy \
|
||||
-l proxy-root \
|
||||
active/container_ddns/install_ddns.yaml
|
||||
```
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Compose
|
||||
|
||||
Put your compose.yaml here.
|
||||
@@ -1,27 +1,35 @@
|
||||
services:
|
||||
litellm:
|
||||
image: docker.litellm.ai/berriai/litellm:main-latest
|
||||
image: docker.litellm.ai/berriai/litellm:main-stable
|
||||
#########################################
|
||||
## Uncomment these lines to start proxy with a config.yaml file ##
|
||||
# volumes:
|
||||
# - ./config.yaml:/app/config.yaml
|
||||
# command:
|
||||
# - "--config=/app/config.yaml"
|
||||
##############################################
|
||||
ports:
|
||||
- 4000:4000
|
||||
env_file: /home/ai/litellm.env
|
||||
- "4000:4000" # Map the container port to the host, change the host port if necessary
|
||||
environment:
|
||||
DATABASE_URL: "postgresql://llmproxy:dbpassword9090@host.containers.internal:5432/litellm"
|
||||
STORE_MODEL_IN_DB: "True"
|
||||
restart: unless-stopped
|
||||
DATABASE_URL: "postgresql://llmproxy:dbpassword9090@db:5432/litellm"
|
||||
STORE_MODEL_IN_DB: "True" # allows adding models to proxy via UI
|
||||
env_file:
|
||||
- ../secrets/litellm.env # Load local .env file
|
||||
depends_on:
|
||||
- litellm-db # Indicates that this service depends on the 'litellm-db' service, ensuring 'litellm-db' starts first
|
||||
healthcheck: # Defines the health check configuration for the container
|
||||
- db # Indicates that this service depends on the 'db' service, ensuring 'db' starts first
|
||||
healthcheck: # Defines the health check configuration for the container
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:4000/health/liveliness')" # Command to execute for health check
|
||||
interval: 30s # Perform health check every 30 seconds
|
||||
timeout: 10s # Health check command times out after 10 seconds
|
||||
retries: 3 # Retry up to 3 times if health check fails
|
||||
start_period: 40s # Wait 40 seconds after container start before beginning health checks
|
||||
- python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:4000/health/liveliness')" # Command to execute for health check
|
||||
interval: 30s # Perform health check every 30 seconds
|
||||
timeout: 10s # Health check command times out after 10 seconds
|
||||
retries: 3 # Retry up to 3 times if health check fails
|
||||
start_period: 40s # Wait 40 seconds after container start before beginning health checks
|
||||
|
||||
litellm-db:
|
||||
db:
|
||||
image: docker.io/postgres:16
|
||||
restart: always
|
||||
container_name: litellm_db
|
||||
environment:
|
||||
POSTGRES_DB: litellm
|
||||
POSTGRES_USER: llmproxy
|
||||
@@ -29,9 +37,26 @@ services:
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- litellm_postgres_data:/var/lib/postgresql/data:z
|
||||
- postgres_data:/var/lib/postgresql/data # Persists Postgres data across container restarts
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -d litellm -U llmproxy"]
|
||||
interval: 1s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
prometheus:
|
||||
image: docker.io/prom/prometheus
|
||||
volumes:
|
||||
- prometheus_data:/prometheus
|
||||
- ../seccrets/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
ports:
|
||||
- "9090:9090"
|
||||
command:
|
||||
- "--config.file=/etc/prometheus/prometheus.yml"
|
||||
- "--storage.tsdb.path=/prometheus"
|
||||
- "--storage.tsdb.retention.time=15d"
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
prometheus_data:
|
||||
postgres_data:
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
# General settings
|
||||
|
||||
general_settings:
|
||||
request_timeout: 600
|
||||
|
||||
# Models
|
||||
model_list:
|
||||
# Qwen3.5-35B variants
|
||||
- model_name: qwen3.5-35b-think-general
|
||||
litellm_params:
|
||||
model: openai/qwen3.5-35b-a3b
|
||||
api_base: https://llama-cpp.reeselink.com
|
||||
api_key: none
|
||||
temperature: 1.0
|
||||
top_p: 0.95
|
||||
presence_penalty: 1.5
|
||||
extra_body:
|
||||
top_k: 20
|
||||
min_p: 0.0
|
||||
repetition_penalty: 1.0
|
||||
chat_template_kwargs:
|
||||
enable_thinking: true
|
||||
|
||||
- model_name: qwen3.5-35b-think-code
|
||||
litellm_params:
|
||||
model: openai/qwen3.5-35b-a3b
|
||||
api_base: https://llama-cpp.reeselink.com
|
||||
api_key: none
|
||||
temperature: 0.6
|
||||
top_p: 0.95
|
||||
presence_penalty: 0.0
|
||||
extra_body:
|
||||
top_k: 20
|
||||
min_p: 0.0
|
||||
repetition_penalty: 1.0
|
||||
chat_template_kwargs:
|
||||
enable_thinking: true
|
||||
|
||||
- model_name: qwen3.5-35b-instruct-general
|
||||
litellm_params:
|
||||
model: openai/qwen3.5-35b-a3b
|
||||
api_base: https://llama-cpp.reeselink.com
|
||||
api_key: none
|
||||
temperature: 0.7
|
||||
top_p: 0.8
|
||||
presence_penalty: 1.5
|
||||
extra_body:
|
||||
top_k: 20
|
||||
min_p: 0.0
|
||||
repetition_penalty: 1.0
|
||||
chat_template_kwargs:
|
||||
enable_thinking: false
|
||||
|
||||
- model_name: qwen3.5-35b-instruct-reasoning
|
||||
litellm_params:
|
||||
model: openai/qwen3.5-35b-a3b
|
||||
api_base: https://llama-cpp.reeselink.com
|
||||
api_key: none
|
||||
temperature: 1.0
|
||||
top_p: 0.95
|
||||
presence_penalty: 1.5
|
||||
extra_body:
|
||||
top_k: 20
|
||||
min_p: 0.0
|
||||
repetition_penalty: 1.0
|
||||
chat_template_kwargs:
|
||||
enable_thinking: false
|
||||
@@ -9,9 +9,8 @@
|
||||
- [Convert litellm compose spec to quadlets](#convert-litellm-compose-spec-to-quadlets)
|
||||
- [Create the litellm.env file](#create-the-litellmenv-file)
|
||||
- [Start and enable your systemd quadlet](#start-and-enable-your-systemd-quadlet)
|
||||
- [Install via Ansible](#install-via-ansible)
|
||||
- [Expose litellm](#expose-litellm)
|
||||
- [Using LiteLLM](#using-litellm)
|
||||
- [Adding Models](#adding-models)
|
||||
- [Testing Models](#testing-models)
|
||||
- [Backup litellm](#backup-litellm)
|
||||
- [Upgrade litellm](#upgrade-litellm)
|
||||
@@ -110,63 +109,28 @@ journalctl --user -u litellm -f
|
||||
systemctl --user enable --now podman-auto-update.timer
|
||||
```
|
||||
|
||||
### Install via Ansible
|
||||
|
||||
Preview changes with a dry run:
|
||||
|
||||
```bash
|
||||
ansible-playbook -i ansible/inventory.yaml active/container_litellm/playbook.yml --check --diff
|
||||
```
|
||||
|
||||
Run the playbook from the Homelab root:
|
||||
|
||||
```bash
|
||||
ansible-playbook -i ansible/inventory.yaml active/container_litellm/playbook.yml
|
||||
```
|
||||
|
||||
This copies the quadlets, config, reloads the systemd user daemon, and starts both `litellm-db` and `litellm` services as the `ai` user.
|
||||
|
||||
### Expose litellm
|
||||
|
||||
1. If you need a domain, follow the [DDNS instructions](/active/container_ddns/ddns.md#install-a-new-ddns-service)
|
||||
2. For a web service, follow the [Caddy instructions](/active/container_caddy/caddy.md#adding-a-new-caddy-record)
|
||||
3. Finally, follow your OS's guide for opening ports via its firewall service.
|
||||
|
||||
## Using LiteLLM
|
||||
|
||||
### Adding Models
|
||||
|
||||
```json
|
||||
// qwen3.5-35b-a3b-thinking
|
||||
{
|
||||
"temperature": 1,
|
||||
"top_p": 0.95,
|
||||
"presence_penalty": 1.5,
|
||||
"extra_body": {
|
||||
"top_k": 20,
|
||||
"min_p": 0,
|
||||
"repetition_penalty": 1,
|
||||
"chat_template_kwargs": {
|
||||
"enable_thinking": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// qwen3.5-35b-a3b-coding
|
||||
{
|
||||
"temperature": 0.6,
|
||||
"top_p": 0.95,
|
||||
"presence_penalty": 0,
|
||||
"extra_body": {
|
||||
"top_k": 20,
|
||||
"min_p": 0,
|
||||
"repetition_penalty": 1,
|
||||
"chat_template_kwargs": {
|
||||
"enable_thinking": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// qwen3.5-35b-a3b-instruct
|
||||
{
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.8,
|
||||
"presence_penalty": 1.5,
|
||||
"extra_body": {
|
||||
"top_k": 20,
|
||||
"min_p": 0,
|
||||
"repetition_penalty": 1,
|
||||
"chat_template_kwargs": {
|
||||
"enable_thinking": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Models
|
||||
|
||||
```bash
|
||||
@@ -191,7 +155,7 @@ curl -L -X POST 'https://aipi.reeseapps.com/v1/chat/completions' \
|
||||
|
||||
## Backup litellm
|
||||
|
||||
Follow the [Borg Backup instructions](/active/systemd_borg/borg.md#set-up-a-client-for-backup)
|
||||
Follow the [Borg Backup instructions](/active/software_borg/borg.md#set-up-a-client-for-backup)
|
||||
|
||||
## Upgrade litellm
|
||||
|
||||
|
||||
86
active/container_litellm/playbook.yml
Normal file
@@ -0,0 +1,86 @@
|
||||
---
|
||||
- name: Install and start LiteLLM quadlets for ai user
|
||||
hosts: ai-ai
|
||||
remote_user: ai
|
||||
|
||||
vars:
|
||||
ai_user: ai
|
||||
quadlets_dir: "/home/{{ ai_user }}/.config/containers/systemd"
|
||||
|
||||
tasks:
|
||||
- name: Ensure ai user home directories exist
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ ai_user }}"
|
||||
group: "{{ ai_user }}"
|
||||
mode: "0755"
|
||||
loop:
|
||||
- "{{ quadlets_dir }}"
|
||||
|
||||
- name: Copy litellm container pod
|
||||
ansible.builtin.copy:
|
||||
src: quadlets/litellm.pod
|
||||
dest: "{{ quadlets_dir }}/litellm.pod"
|
||||
owner: "{{ ai_user }}"
|
||||
group: "{{ ai_user }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Copy litellm container quadlet
|
||||
ansible.builtin.copy:
|
||||
src: quadlets/litellm-web.container
|
||||
dest: "{{ quadlets_dir }}/litellm-web.container"
|
||||
owner: "{{ ai_user }}"
|
||||
group: "{{ ai_user }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Copy litellm-db container quadlet
|
||||
ansible.builtin.copy:
|
||||
src: quadlets/litellm-db.container
|
||||
dest: "{{ quadlets_dir }}/litellm-db.container"
|
||||
owner: "{{ ai_user }}"
|
||||
group: "{{ ai_user }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Copy prometheus container quadlet
|
||||
ansible.builtin.copy:
|
||||
src: quadlets/litellm-prometheus.container
|
||||
dest: "{{ quadlets_dir }}/litellm-prometheus.container"
|
||||
owner: "{{ ai_user }}"
|
||||
group: "{{ ai_user }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Copy prometheus config
|
||||
ansible.builtin.copy:
|
||||
src: secrets/litellm-prometheus.yaml
|
||||
dest: "/home/{{ ai_user }}/litellm-prometheus.yaml"
|
||||
owner: "{{ ai_user }}"
|
||||
group: "{{ ai_user }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Copy litellm.env file
|
||||
ansible.builtin.copy:
|
||||
src: secrets/litellm.env
|
||||
dest: "/home/{{ ai_user }}/litellm.env"
|
||||
owner: "{{ ai_user }}"
|
||||
group: "{{ ai_user }}"
|
||||
mode: "0600"
|
||||
|
||||
- name: Copy litellm-config.yaml
|
||||
ansible.builtin.copy:
|
||||
src: secrets/litellm-config.yaml
|
||||
dest: "/home/{{ ai_user }}/litellm-config.yaml"
|
||||
owner: "{{ ai_user }}"
|
||||
group: "{{ ai_user }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Reload systemd user daemon
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
scope: user
|
||||
|
||||
- name: Restart litellm pod
|
||||
ansible.builtin.systemd:
|
||||
name: litellm-pod
|
||||
state: restarted
|
||||
scope: user
|
||||
@@ -1,12 +1,13 @@
|
||||
[Container]
|
||||
Pod=litellm.pod
|
||||
ContainerName=litellm-db
|
||||
Environment=POSTGRES_DB=litellm POSTGRES_USER=llmproxy POSTGRES_PASSWORD=dbpassword9090
|
||||
HealthCmd='pg_isready -d litellm -U llmproxy'
|
||||
HealthInterval=1s
|
||||
HealthRetries=10
|
||||
HealthTimeout=5s
|
||||
Image=docker.io/postgres:16
|
||||
PublishPort=5432:5432
|
||||
Volume=litellm_postgres_data:/var/lib/postgresql/data:z
|
||||
Volume=litellm_postgres_data:/var/lib/postgresql/data
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
[Container]
|
||||
Pod=litellm.pod
|
||||
ContainerName=litellm-prom
|
||||
Exec='--config.file=/etc/prometheus/prometheus.yml' '--storage.tsdb.path=/prometheus' '--storage.tsdb.retention.time=15d'
|
||||
Image=docker.io/prom/prometheus
|
||||
Volume=litellm_prometheus_data:/prometheus
|
||||
Volume=/home/ai/litellm-prometheus.yaml:/etc/prometheus/prometheus.yml:z
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -2,18 +2,18 @@
|
||||
Requires=litellm-db.service
|
||||
|
||||
[Container]
|
||||
Environment=DATABASE_URL=postgresql://llmproxy:dbpassword9090@host.containers.internal:5432/litellm STORE_MODEL_IN_DB=True
|
||||
Pod=litellm.pod
|
||||
ContainerName=litellm-web
|
||||
Environment=DATABASE_URL=postgresql://llmproxy:dbpassword9090@localhost:5432/litellm STORE_MODEL_IN_DB=True
|
||||
EnvironmentFile=/home/ai/litellm.env
|
||||
HealthCmd="python3 -c \"import urllib.request; urllib.request.urlopen('http://localhost:4000/health/liveliness')\""
|
||||
HealthInterval=30s
|
||||
HealthRetries=3
|
||||
HealthStartPeriod=40s
|
||||
HealthTimeout=10s
|
||||
Image=docker.litellm.ai/berriai/litellm:main-latest
|
||||
PublishPort=4000:4000
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
Image=ghcr.io/berriai/litellm-database:v1.83.14-stable.patch.3
|
||||
Volume=/home/ai/litellm-config.yaml:/app/config.yaml:z
|
||||
Exec=--config=/app/config.yaml
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
3
active/container_litellm/quadlets/litellm.pod
Normal file
@@ -0,0 +1,3 @@
|
||||
[Pod]
|
||||
# litellm web interface
|
||||
PublishPort=4000:4000/tcp
|
||||
@@ -3,6 +3,7 @@
|
||||
- [Driveripper](#driveripper)
|
||||
- [General Principles](#general-principles)
|
||||
- [Important Locations](#important-locations)
|
||||
- [Backups](#backups)
|
||||
- [Monitoring Scripts](#monitoring-scripts)
|
||||
- [Quick Ansible Commands](#quick-ansible-commands)
|
||||
- [Quickstart VM](#quickstart-vm)
|
||||
@@ -12,7 +13,7 @@
|
||||
- [Best Practices](#best-practices)
|
||||
- [OSBuild Composer](#osbuild-composer)
|
||||
- [Retired Disks](#retired-disks)
|
||||
- [Sending emails](#sending-emails)
|
||||
- [Sending emails](#sending-emails)
|
||||
|
||||
## General Principles
|
||||
|
||||
@@ -27,6 +28,28 @@
|
||||
- `/etc/luks-keys`: luks keys
|
||||
- `/usr/local/scripts`: admin scripts
|
||||
|
||||
## Backups
|
||||
|
||||
```bash
|
||||
# smb
|
||||
rsync -av --progress \
|
||||
--exclude .snapshots \
|
||||
/srv/smb/ \
|
||||
/srv/backup/smb/
|
||||
|
||||
# archive
|
||||
rsync -av --progress \
|
||||
--exclude .snapshots \
|
||||
/srv/archive/ \
|
||||
/srv/backup/archive/
|
||||
|
||||
# vm
|
||||
rsync -av --progress \
|
||||
--exclude .snapshots \
|
||||
/srv/vm/ \
|
||||
/srv/backup/vm/
|
||||
```
|
||||
|
||||
## Monitoring Scripts
|
||||
|
||||
```bash
|
||||
@@ -196,7 +219,7 @@ Retired 12-19-2025
|
||||
|
||||

|
||||
|
||||
### Sending emails
|
||||
## Sending emails
|
||||
|
||||
```bash
|
||||
# s-nail is mailx
|
||||
|
||||
BIN
active/device_esphome/beep.wav
Normal file
@@ -8,6 +8,7 @@
|
||||
- [Adding a New Device](#adding-a-new-device)
|
||||
- [Controlling Home Assistant](#controlling-home-assistant)
|
||||
- [Configuration Sections](#configuration-sections)
|
||||
- [Symbols](#symbols)
|
||||
- [esphome](#esphome)
|
||||
- [esp32](#esp32-1)
|
||||
- [logger](#logger)
|
||||
@@ -63,7 +64,8 @@ uv venv
|
||||
uv pip install esphome
|
||||
source .venv/bin/activate
|
||||
|
||||
esphome run m5stack-atom-echo.yaml
|
||||
# grep for debug logs only (helpful for filtering noise)
|
||||
esphome run tab1.yaml | grep -E '.*[\[D\]]'
|
||||
```
|
||||
|
||||
## Adding a New Device
|
||||
@@ -78,6 +80,13 @@ esphome run m5stack-atom-echo.yaml
|
||||
|
||||
<https://esphome.io/components/>
|
||||
|
||||
## Symbols
|
||||
|
||||
You can display the embedded symbols among the text by their codepoint address
|
||||
preceded by \u. For example: \uF00C :
|
||||
|
||||

|
||||
|
||||
### esphome
|
||||
|
||||
### esp32
|
||||
@@ -145,6 +154,22 @@ data:
|
||||
media_content_id: "media-source://media_source/local/wake_word_triggered.wav"
|
||||
```
|
||||
|
||||
Playing arbitrary sound:
|
||||
|
||||
```yaml
|
||||
audio_file:
|
||||
- id: beep_sound
|
||||
file: "beep.wav"
|
||||
|
||||
media_source:
|
||||
- platform: audio_file
|
||||
id: file_source
|
||||
|
||||
- media_player.speaker.play_on_device_media_file:
|
||||
media_file: beep_sound
|
||||
announcement: true
|
||||
```
|
||||
|
||||
### voice assistant
|
||||
|
||||
<https://esphome.io/components/voice_assistant/>
|
||||
|
||||
@@ -54,24 +54,24 @@ i2c:
|
||||
id: bus_1
|
||||
|
||||
sensor:
|
||||
- platform: sonic_i2c
|
||||
i2c_id: bus_1
|
||||
address: 0x57
|
||||
name: "Ultrasonic Sensor 1"
|
||||
id: ultrasonic1
|
||||
unit_of_measurement: mm
|
||||
update_interval: 5s
|
||||
filters:
|
||||
- filter_out: nan
|
||||
- lambda: |-
|
||||
if (x == 0) {
|
||||
return {}; // This filters out the reading
|
||||
} else {
|
||||
return x; // This passes the reading through
|
||||
}
|
||||
- sliding_window_moving_average:
|
||||
window_size: 10
|
||||
send_every: 20
|
||||
- platform: sonic_i2c
|
||||
i2c_id: bus_1
|
||||
address: 0x57
|
||||
name: "Ultrasonic Sensor 1"
|
||||
id: ultrasonic1
|
||||
unit_of_measurement: mm
|
||||
update_interval: 5s
|
||||
filters:
|
||||
- filter_out: nan
|
||||
- lambda: |-
|
||||
if (x == 0) {
|
||||
return {}; // This filters out the reading
|
||||
} else {
|
||||
return x; // This passes the reading through
|
||||
}
|
||||
- sliding_window_moving_average:
|
||||
window_size: 10
|
||||
send_every: 20
|
||||
|
||||
button:
|
||||
- platform: factory_reset
|
||||
@@ -99,7 +99,7 @@ speaker:
|
||||
dac_type: external
|
||||
bits_per_sample: 16bit
|
||||
sample_rate: 16000
|
||||
channel: stereo # The Echo has poor playback audio quality when using mon audio
|
||||
channel: stereo # The Echo has poor playback audio quality when using mon audio
|
||||
buffer_duration: 60ms
|
||||
|
||||
media_player:
|
||||
@@ -197,7 +197,7 @@ voice_assistant:
|
||||
- delay: 2s
|
||||
- script.execute: reset_led
|
||||
on_client_connected:
|
||||
- delay: 2s # Give the api server time to settle
|
||||
- delay: 2s # Give the api server time to settle
|
||||
- script.execute: start_wake_word
|
||||
on_client_disconnected:
|
||||
- script.execute: stop_wake_word
|
||||
@@ -355,24 +355,24 @@ switch:
|
||||
on_turn_off:
|
||||
# Turn off the repeat mode and disable the pause between playlist items
|
||||
- lambda: |-
|
||||
id(echo_media_player)
|
||||
->make_call()
|
||||
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_OFF)
|
||||
.set_announcement(true)
|
||||
.perform();
|
||||
id(echo_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 0);
|
||||
id(echo_media_player)
|
||||
->make_call()
|
||||
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_OFF)
|
||||
.set_announcement(true)
|
||||
.perform();
|
||||
id(echo_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 0);
|
||||
# Stop playing the alarm
|
||||
- media_player.stop:
|
||||
announcement: true
|
||||
on_turn_on:
|
||||
# Turn on the repeat mode and pause for 1000 ms between playlist items/repeats
|
||||
- lambda: |-
|
||||
id(echo_media_player)
|
||||
->make_call()
|
||||
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_ONE)
|
||||
.set_announcement(true)
|
||||
.perform();
|
||||
id(echo_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 1000);
|
||||
id(echo_media_player)
|
||||
->make_call()
|
||||
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_ONE)
|
||||
.set_announcement(true)
|
||||
.perform();
|
||||
id(echo_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 1000);
|
||||
- media_player.speaker.play_on_device_media_file:
|
||||
media_file: timer_finished_wave_file
|
||||
announcement: true
|
||||
|
||||
BIN
active/device_esphome/image.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
active/device_esphome/images/charging.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
active/device_esphome/images/charging.xcf
Normal file
|
Before Width: | Height: | Size: 277 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 431 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 430 KiB After Width: | Height: | Size: 109 KiB |
997
active/device_esphome/pyramid1.yaml
Normal file
@@ -0,0 +1,997 @@
|
||||
---
|
||||
substitutions:
|
||||
name: pyramid1
|
||||
friendly_name: Pyramid 1
|
||||
|
||||
# Casita images
|
||||
loading_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/loading_320_240.png
|
||||
idle_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/idle_320_240.png
|
||||
listening_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/listening_320_240.png
|
||||
thinking_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/thinking_320_240.png
|
||||
replying_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/replying_320_240.png
|
||||
error_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/casita/error_320_240.png
|
||||
error_no_wifi_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/error_box_illustrations/error-no-wifi.png
|
||||
error_no_ha_illustration_file: https://github.com/esphome/wake-word-voice-assistants/raw/main/error_box_illustrations/error-no-ha.png
|
||||
|
||||
# Fonts
|
||||
mdi_webfont_file: https://raw.githubusercontent.com/Templarian/MaterialDesign-Webfont/master/fonts/materialdesignicons-webfont.ttf
|
||||
|
||||
# Audio files
|
||||
wake_word_trigger_sound_file: wake_word_triggered.wav
|
||||
# timer_finished_sound_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/timer_finished.flac
|
||||
# error_cloud_expired_file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/error_cloud_expired.mp3
|
||||
|
||||
# Micro wake word models
|
||||
pick_pig: https://raw.githubusercontent.com/esphome/micro-wake-word-models/refs/heads/main/models/v2/experiments/hey_peppa_pig.json
|
||||
stop_model_file: https://github.com/kahrendt/microWakeWord/releases/download/stop/stop.json
|
||||
# Background colors
|
||||
loading_illustration_background_color: "000000"
|
||||
idle_illustration_background_color: "000000"
|
||||
listening_illustration_background_color: "FFFFFF"
|
||||
thinking_illustration_background_color: "FFFFFF"
|
||||
replying_illustration_background_color: "FFFFFF"
|
||||
error_illustration_background_color: "000000"
|
||||
|
||||
# Phases of the Voice Assistant
|
||||
# The voice assistant is ready to be triggered by a wake word
|
||||
voice_assist_idle_phase_id: "1"
|
||||
# The voice assistant is listening for a voice command
|
||||
voice_assist_listening_phase_id: "2"
|
||||
# The voice assistant is currently processing the command
|
||||
voice_assist_thinking_phase_id: "3"
|
||||
# The voice assistant is replying to the command
|
||||
voice_assist_replying_phase_id: "4"
|
||||
# The voice assistant is not ready
|
||||
voice_assist_not_ready_phase_id: "10"
|
||||
# The voice assistant encountered an error
|
||||
voice_assist_error_phase_id: "11"
|
||||
# Muted phase
|
||||
voice_assist_muted_phase_id: "12"
|
||||
# Finished timer phase
|
||||
voice_assist_timer_finished_phase_id: "20"
|
||||
|
||||
esphome:
|
||||
name: pyramid1
|
||||
friendly_name: Pyramid 1
|
||||
min_version: 2025.11.3
|
||||
on_boot:
|
||||
priority: 600
|
||||
then:
|
||||
- delay: 30s
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(init_in_progress);
|
||||
then:
|
||||
- lambda: id(init_in_progress) = false;
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
flash_size: 8MB
|
||||
cpu_frequency: 240MHz
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
api:
|
||||
encryption:
|
||||
key: "innoIL7I6ZfRekL58F65REjeYNLW1Hp/Q/Kv9SEjnNA="
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
password: "22de00dcf5c2701a25d2fe719d596123"
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
|
||||
ap:
|
||||
ssid: "Echo-Pyramid Fallback Hotspot"
|
||||
password: "uSTvJjVzweZp"
|
||||
|
||||
# Enable logging
|
||||
logger:
|
||||
level: INFO
|
||||
logs:
|
||||
sensor: WARN
|
||||
|
||||
captive_portal:
|
||||
|
||||
button:
|
||||
- platform: factory_reset
|
||||
id: factory_reset_btn
|
||||
internal: true
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
pin:
|
||||
number: GPIO41
|
||||
mode: INPUT_PULLUP
|
||||
inverted: true
|
||||
id: user_button
|
||||
internal: true
|
||||
on_multi_click:
|
||||
- timing:
|
||||
- ON for at least 50ms
|
||||
- OFF for at least 50ms
|
||||
then:
|
||||
- switch.turn_off: timer_ringing
|
||||
- timing:
|
||||
- ON for at least 10s
|
||||
then:
|
||||
- button.press: factory_reset_btn
|
||||
|
||||
external_components:
|
||||
- source: github://m5stack/esphome-yaml/components
|
||||
components: [aw87559, si5351, lp5562, pyramidrgb, pyramidtouch]
|
||||
refresh: 0s
|
||||
|
||||
# I2C Bus Configuration
|
||||
i2c:
|
||||
- id: bsp_bus
|
||||
sda: GPIO45
|
||||
scl: GPIO0
|
||||
scan: true
|
||||
- id: ext_bus # used on atomic echo base
|
||||
sda: GPIO38
|
||||
scl: GPIO39
|
||||
|
||||
# Ehco Base GPIO Expander
|
||||
pi4ioe5v6408:
|
||||
- id: pi4ioe5v6408_hub
|
||||
i2c_id: ext_bus
|
||||
address: 0x43
|
||||
|
||||
aw87559:
|
||||
id: audio_amp
|
||||
i2c_id: ext_bus
|
||||
address: 0x5B
|
||||
|
||||
si5351:
|
||||
id: clock_gen
|
||||
i2c_id: ext_bus
|
||||
address: 0x60
|
||||
|
||||
# I2S Bus Configuration
|
||||
i2s_audio:
|
||||
- id: i2s_audio_bus
|
||||
i2s_lrclk_pin: GPIO8
|
||||
i2s_bclk_pin: GPIO6
|
||||
|
||||
spi:
|
||||
clk_pin: GPIO15
|
||||
mosi_pin: GPIO21
|
||||
# miso_pin is not used
|
||||
|
||||
audio_dac:
|
||||
- platform: es8311
|
||||
id: es8311_dac
|
||||
i2c_id: ext_bus
|
||||
bits_per_sample: 16bit
|
||||
sample_rate: 16000
|
||||
|
||||
audio_adc:
|
||||
- platform: es7210
|
||||
id: es7210_adc
|
||||
i2c_id: ext_bus
|
||||
address: 0x40
|
||||
bits_per_sample: 16bit
|
||||
sample_rate: 16000
|
||||
|
||||
microphone:
|
||||
- platform: i2s_audio
|
||||
id: i2s_mic
|
||||
sample_rate: 16000
|
||||
i2s_din_pin: GPIO5
|
||||
bits_per_sample: 16bit
|
||||
adc_type: external
|
||||
channel: stereo
|
||||
|
||||
speaker:
|
||||
- platform: i2s_audio
|
||||
id: i2s_speaker
|
||||
i2s_dout_pin: GPIO7
|
||||
dac_type: external
|
||||
bits_per_sample: 16bit
|
||||
sample_rate: 16000
|
||||
channel: mono
|
||||
audio_dac: es8311_dac
|
||||
|
||||
media_player:
|
||||
- platform: speaker
|
||||
name: "Echo Pyramid Player"
|
||||
id: echo_pyramid_player
|
||||
volume_min: 0.0
|
||||
volume_max: 1.0
|
||||
volume_initial: 0.10
|
||||
buffer_size: 6000
|
||||
announcement_pipeline:
|
||||
speaker: i2s_speaker
|
||||
format: WAV
|
||||
# sample_rate: 48000
|
||||
# num_channels: 1
|
||||
codec_support_enabled: false
|
||||
files:
|
||||
- id: wake_word_triggered_sound
|
||||
file: ${wake_word_trigger_sound_file}
|
||||
# - id: timer_finished_sound
|
||||
# file: ${timer_finished_sound_file}
|
||||
# - id: error_cloud_expired
|
||||
# file: ${error_cloud_expired_file}
|
||||
on_state:
|
||||
- logger.log: "State updated!"
|
||||
on_play:
|
||||
- logger.log: "Playback started!"
|
||||
|
||||
on_announcement:
|
||||
- logger.log: "Announcing!"
|
||||
# Stop the wake word (mWW or VA) if the mic is capturing
|
||||
- if:
|
||||
condition:
|
||||
- microphone.is_capturing:
|
||||
then:
|
||||
- script.execute: stop_wake_word
|
||||
# Ensure VA stops before moving on
|
||||
- if:
|
||||
condition:
|
||||
- lambda: |-
|
||||
return id(wake_word_engine_location).current_option() == "In Home Assistant";
|
||||
then:
|
||||
- wait_until:
|
||||
- not:
|
||||
voice_assistant.is_running:
|
||||
# Since VA isn't running, this is user-intiated media playback. Draw the mute display
|
||||
- if:
|
||||
condition:
|
||||
not:
|
||||
voice_assistant.is_running:
|
||||
then:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id};
|
||||
- script.execute: draw_display
|
||||
on_idle:
|
||||
# Since VA isn't running, this is the end of user-intiated media playback. Restart the wake word.
|
||||
- if:
|
||||
condition:
|
||||
not:
|
||||
voice_assistant.is_running:
|
||||
then:
|
||||
- script.execute: start_wake_word
|
||||
- script.execute: set_idle_or_mute_phase
|
||||
- script.execute: draw_display
|
||||
|
||||
switch:
|
||||
# NS4150B
|
||||
- platform: gpio
|
||||
name: Speaker Enable
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe5v6408_hub
|
||||
number: 0
|
||||
mode:
|
||||
output: true
|
||||
icon: "mdi:volume-high"
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
|
||||
- platform: template
|
||||
name: Mute Microphone
|
||||
id: mute
|
||||
icon: "mdi:microphone-off"
|
||||
optimistic: true
|
||||
restore_mode: RESTORE_DEFAULT_OFF
|
||||
entity_category: config
|
||||
on_turn_off:
|
||||
- microphone.unmute:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
|
||||
|
||||
on_turn_on:
|
||||
- microphone.mute:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id};
|
||||
|
||||
- platform: template
|
||||
id: timer_ringing
|
||||
optimistic: true
|
||||
internal: true
|
||||
restore_mode: ALWAYS_OFF
|
||||
on_turn_off:
|
||||
# Turn off the repeat mode and disable the pause between playlist items
|
||||
- lambda: |-
|
||||
id(echo_pyramid_player)
|
||||
->make_call()
|
||||
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_OFF)
|
||||
.set_announcement(true)
|
||||
.perform();
|
||||
id(echo_pyramid_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 0);
|
||||
# Stop playing the alarm
|
||||
- media_player.stop:
|
||||
announcement: true
|
||||
- script.execute: start_wake_word
|
||||
on_turn_on:
|
||||
- script.execute: stop_wake_word
|
||||
# Turn on the repeat mode and pause for 1000 ms between playlist items/repeats
|
||||
- lambda: |-
|
||||
id(echo_pyramid_player)
|
||||
->make_call()
|
||||
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_ONE)
|
||||
.set_announcement(true)
|
||||
.perform();
|
||||
id(echo_pyramid_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 1000);
|
||||
# - media_player.speaker.play_on_device_media_file:
|
||||
# media_file: timer_finished_sound
|
||||
# announcement: true
|
||||
- delay: 15min
|
||||
- switch.turn_off: timer_ringing
|
||||
|
||||
select:
|
||||
- platform: template
|
||||
entity_category: config
|
||||
name: Wake word engine location
|
||||
id: wake_word_engine_location
|
||||
icon: "mdi:account-voice"
|
||||
optimistic: true
|
||||
restore_value: true
|
||||
options:
|
||||
- In Home Assistant
|
||||
- On device
|
||||
initial_option: On device
|
||||
on_value:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return !id(init_in_progress);
|
||||
then:
|
||||
- wait_until:
|
||||
lambda: return id(voice_assistant_phase) == ${voice_assist_muted_phase_id} || id(voice_assistant_phase) == ${voice_assist_idle_phase_id};
|
||||
- if:
|
||||
condition:
|
||||
lambda: return x == "In Home Assistant";
|
||||
then:
|
||||
- micro_wake_word.stop
|
||||
- delay: 500ms
|
||||
- if:
|
||||
condition:
|
||||
switch.is_off: mute
|
||||
then:
|
||||
- lambda: id(va).set_use_wake_word(true);
|
||||
- voice_assistant.start_continuous:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return x == "On device";
|
||||
then:
|
||||
- lambda: id(va).set_use_wake_word(false);
|
||||
- voice_assistant.stop
|
||||
- delay: 500ms
|
||||
- if:
|
||||
condition:
|
||||
switch.is_off: mute
|
||||
then:
|
||||
- micro_wake_word.start
|
||||
|
||||
- platform: template
|
||||
name: "Wake word sensitivity"
|
||||
optimistic: true
|
||||
initial_option: Slightly sensitive
|
||||
restore_value: true
|
||||
entity_category: config
|
||||
options:
|
||||
- Slightly sensitive
|
||||
- Moderately sensitive
|
||||
- Very sensitive
|
||||
on_value:
|
||||
# Sets specific wake word probabilities computed for each particular model
|
||||
# Note probability cutoffs are set as a quantized uint8 value, each comment has the corresponding floating point cutoff
|
||||
# False Accepts per Hour values are tested against all units and channels from the Dinner Party Corpus.
|
||||
# These cutoffs apply only to the specific models included in the firmware: okay_nabu@20241226.3, hey_jarvis@v2, hey_mycroft@v2
|
||||
lambda: |-
|
||||
if (x == "Slightly sensitive") {
|
||||
id(okay_nabu).set_probability_cutoff(217); // 0.85 -> 0.000 FAPH on DipCo (Manifest's default)
|
||||
id(hey_jarvis).set_probability_cutoff(247); // 0.97 -> 0.563 FAPH on DipCo (Manifest's default)
|
||||
id(hey_mycroft).set_probability_cutoff(253); // 0.99 -> 0.567 FAPH on DipCo
|
||||
} else if (x == "Moderately sensitive") {
|
||||
id(okay_nabu).set_probability_cutoff(176); // 0.69 -> 0.376 FAPH on DipCo
|
||||
id(hey_jarvis).set_probability_cutoff(235); // 0.92 -> 0.939 FAPH on DipCo
|
||||
id(hey_mycroft).set_probability_cutoff(242); // 0.95 -> 1.502 FAPH on DipCo (Manifest's default)
|
||||
} else if (x == "Very sensitive") {
|
||||
id(okay_nabu).set_probability_cutoff(143); // 0.56 -> 0.751 FAPH on DipCo
|
||||
id(hey_jarvis).set_probability_cutoff(212); // 0.83 -> 1.502 FAPH on DipCo
|
||||
id(hey_mycroft).set_probability_cutoff(237); // 0.93 -> 1.878 FAPH on DipCo
|
||||
}
|
||||
|
||||
micro_wake_word:
|
||||
id: mww
|
||||
microphone: i2s_mic
|
||||
models:
|
||||
- model: okay_nabu
|
||||
id: okay_nabu
|
||||
- model: hey_jarvis
|
||||
id: hey_jarvis
|
||||
- model: hey_mycroft
|
||||
id: hey_mycroft
|
||||
- model: https://raw.githubusercontent.com/esphome/micro-wake-word-models/refs/heads/main/models/v2/experiments/hey_peppa_pig.json
|
||||
id: hey_peppa_pig
|
||||
- model: ${stop_model_file}
|
||||
id: stop
|
||||
internal: true
|
||||
vad:
|
||||
on_wake_word_detected:
|
||||
- script.execute:
|
||||
id: play_sound
|
||||
priority: true
|
||||
sound_file: !lambda return id(wake_word_triggered_sound);
|
||||
|
||||
- wait_until:
|
||||
condition:
|
||||
- media_player.is_announcing:
|
||||
timeout: 0.5s
|
||||
# Announcement is finished and the I2S bus is free
|
||||
- wait_until:
|
||||
- and:
|
||||
- not:
|
||||
media_player.is_announcing:
|
||||
- not:
|
||||
speaker.is_playing:
|
||||
|
||||
- voice_assistant.start:
|
||||
wake_word: !lambda return wake_word;
|
||||
|
||||
voice_assistant:
|
||||
id: va
|
||||
microphone: i2s_mic
|
||||
media_player: echo_pyramid_player
|
||||
micro_wake_word: mww
|
||||
noise_suppression_level: 2
|
||||
auto_gain: 31dBFS
|
||||
volume_multiplier: 2.0
|
||||
on_listening:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_listening_phase_id};
|
||||
- script.execute: draw_display
|
||||
on_stt_vad_end:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_thinking_phase_id};
|
||||
- script.execute: draw_display
|
||||
on_tts_start:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_replying_phase_id};
|
||||
- script.execute: draw_display
|
||||
on_end:
|
||||
# Wait a short amount of time to see if an announcement starts
|
||||
- wait_until:
|
||||
condition:
|
||||
- media_player.is_announcing:
|
||||
timeout: 0.5s
|
||||
# Announcement is finished and the I2S bus is free
|
||||
- wait_until:
|
||||
- and:
|
||||
- not:
|
||||
media_player.is_announcing:
|
||||
- not:
|
||||
speaker.is_playing:
|
||||
# Restart only mWW if enabled; streaming wake words automatically restart
|
||||
- if:
|
||||
condition:
|
||||
- lambda: |-
|
||||
return id(wake_word_engine_location).current_option() == "On device";
|
||||
then:
|
||||
- lambda: id(va).set_use_wake_word(false);
|
||||
- micro_wake_word.start:
|
||||
- script.execute: set_idle_or_mute_phase
|
||||
- script.execute: draw_display
|
||||
|
||||
on_error:
|
||||
# Only set the error phase if the error code is different than duplicate_wake_up_detected or stt-no-text-recognized
|
||||
# These two are ignored for a better user experience
|
||||
- if:
|
||||
condition:
|
||||
and:
|
||||
- lambda: return !id(init_in_progress);
|
||||
- lambda: return code != "duplicate_wake_up_detected";
|
||||
- lambda: return code != "stt-no-text-recognized";
|
||||
then:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_error_phase_id};
|
||||
- script.execute: draw_display
|
||||
- delay: 1s
|
||||
- if:
|
||||
condition:
|
||||
switch.is_off: mute
|
||||
then:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
|
||||
else:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id};
|
||||
# If the error code is cloud-auth-failed, serve a local audio file guiding the user.
|
||||
- if:
|
||||
condition:
|
||||
- lambda: return code == "cloud-auth-failed";
|
||||
then:
|
||||
# - script.execute:
|
||||
# id: play_sound
|
||||
# priority: true
|
||||
# sound_file: !lambda return id(error_cloud_expired);
|
||||
- script.execute: draw_display
|
||||
|
||||
on_client_connected:
|
||||
- lambda: id(init_in_progress) = false;
|
||||
- script.execute: start_wake_word
|
||||
- script.execute: set_idle_or_mute_phase
|
||||
- script.execute: draw_display
|
||||
|
||||
on_client_disconnected:
|
||||
- script.execute: stop_wake_word
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_not_ready_phase_id};
|
||||
- script.execute: draw_display
|
||||
|
||||
on_timer_finished:
|
||||
- switch.turn_on: timer_ringing
|
||||
- wait_until:
|
||||
media_player.is_announcing:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_timer_finished_phase_id};
|
||||
|
||||
globals:
|
||||
- id: init_in_progress
|
||||
type: bool
|
||||
restore_value: false
|
||||
initial_value: "true"
|
||||
- id: voice_assistant_phase
|
||||
type: int
|
||||
restore_value: false
|
||||
initial_value: ${voice_assist_not_ready_phase_id}
|
||||
- id: current_volume
|
||||
type: float
|
||||
restore_value: true
|
||||
initial_value: "0.3"
|
||||
|
||||
sensor:
|
||||
- platform: pyramidtouch
|
||||
address: 0x1A
|
||||
i2c_id: ext_bus
|
||||
update_interval: 50ms
|
||||
publish_swipe_event: true
|
||||
swipe_timeout_ms: 500
|
||||
touch1:
|
||||
name: "Touch 1"
|
||||
touch2:
|
||||
name: "Touch 2"
|
||||
touch3:
|
||||
name: "Touch 3"
|
||||
touch4:
|
||||
name: "Touch 4"
|
||||
swipe_event:
|
||||
name: "Touch Swipe Event"
|
||||
entity_category: diagnostic
|
||||
on_value:
|
||||
then:
|
||||
- lambda: |-
|
||||
// Swipe codes:
|
||||
// 1 = Left Up (volume up)
|
||||
// 2 = Left Down (volume down)
|
||||
// 3 = Right Up (brightness up)
|
||||
// 4 = Right Down (brightness down)
|
||||
const float volume_step = 0.05f; // 5% volume per gesture
|
||||
const float brightness_step = 5.0f; // 5% brightness per gesture
|
||||
|
||||
const int ev = (int) x;
|
||||
|
||||
if (ev == 1 || ev == 2) {
|
||||
// Left side: control volume (0.0 - 1.0)
|
||||
float v = id(current_volume);
|
||||
if (ev == 1) {
|
||||
v = std::min(1.0f, v + volume_step);
|
||||
} else {
|
||||
v = std::max(0.0f, v - volume_step);
|
||||
}
|
||||
|
||||
auto call = id(echo_pyramid_player).make_call();
|
||||
call.set_volume(v);
|
||||
call.perform();
|
||||
|
||||
id(current_volume) = v;
|
||||
} else if (ev == 3 || ev == 4) {
|
||||
// Right side: control RGB brightness (0 - 100)
|
||||
float b = id(rgb_master_brightness).state;
|
||||
if (ev == 3) {
|
||||
b = std::min(100.0f, b + brightness_step);
|
||||
} else {
|
||||
b = std::max(0.0f, b - brightness_step);
|
||||
}
|
||||
|
||||
uint8_t b8 = (uint8_t) b;
|
||||
id(pyramid_rgb1).set_strip_brightness(1, b8);
|
||||
id(pyramid_rgb2).set_strip_brightness(2, b8);
|
||||
id(rgb_master_brightness).publish_state(b);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
lp5562:
|
||||
id: lp5562_led
|
||||
i2c_id: bsp_bus
|
||||
use_internal_clk: true
|
||||
# power_save_mode: true
|
||||
# high_pwm_freq: true
|
||||
# logarithmic_dimming: true
|
||||
white_current: 17.5
|
||||
|
||||
pyramidrgb:
|
||||
- id: pyramid_rgb1
|
||||
i2c_id: ext_bus
|
||||
address: 0x1A
|
||||
strip: 1
|
||||
brightness: 80
|
||||
- id: pyramid_rgb2
|
||||
i2c_id: ext_bus
|
||||
address: 0x1A
|
||||
strip: 2
|
||||
brightness: 80
|
||||
|
||||
number:
|
||||
# Master media player volume (0.0–1.0)
|
||||
- platform: template
|
||||
name: "Master Volume"
|
||||
id: master_volume
|
||||
icon: "mdi:volume-high"
|
||||
min_value: 0.0
|
||||
max_value: 0.4
|
||||
step: 0.01
|
||||
restore_value: true
|
||||
initial_value: 0.3
|
||||
optimistic: true
|
||||
set_action:
|
||||
- lambda: |-
|
||||
float v = x;
|
||||
auto call = id(echo_pyramid_player).make_call();
|
||||
call.set_volume(v);
|
||||
call.perform();
|
||||
id(current_volume) = v;
|
||||
|
||||
# Master RGB brightness (applies to both strips, 0–100%)
|
||||
- platform: template
|
||||
name: "RGB Master Brightness"
|
||||
id: rgb_master_brightness
|
||||
icon: "mdi:brightness-6"
|
||||
min_value: 0
|
||||
max_value: 100
|
||||
step: 1
|
||||
restore_value: true
|
||||
initial_value: 100
|
||||
optimistic: true
|
||||
set_action:
|
||||
- lambda: |-
|
||||
uint8_t b = (uint8_t) x;
|
||||
id(pyramid_rgb1).set_strip_brightness(1, b);
|
||||
id(pyramid_rgb2).set_strip_brightness(2, b);
|
||||
|
||||
output:
|
||||
- platform: lp5562
|
||||
id: lp5562_white_channel
|
||||
lp5562_id: lp5562_led
|
||||
channel: white
|
||||
- platform: pyramidrgb
|
||||
id: rgb1_ch0_red
|
||||
pyramidrgb_id: pyramid_rgb1
|
||||
channel: 0
|
||||
color: red
|
||||
- platform: pyramidrgb
|
||||
id: rgb1_ch0_green
|
||||
pyramidrgb_id: pyramid_rgb1
|
||||
channel: 0
|
||||
color: green
|
||||
- platform: pyramidrgb
|
||||
id: rgb1_ch0_blue
|
||||
pyramidrgb_id: pyramid_rgb1
|
||||
channel: 0
|
||||
color: blue
|
||||
# Strip 1, Channel 1 (Group 2)
|
||||
- platform: pyramidrgb
|
||||
id: rgb1_ch1_red
|
||||
pyramidrgb_id: pyramid_rgb1
|
||||
channel: 1
|
||||
color: red
|
||||
- platform: pyramidrgb
|
||||
id: rgb1_ch1_green
|
||||
pyramidrgb_id: pyramid_rgb1
|
||||
channel: 1
|
||||
color: green
|
||||
- platform: pyramidrgb
|
||||
id: rgb1_ch1_blue
|
||||
pyramidrgb_id: pyramid_rgb1
|
||||
channel: 1
|
||||
color: blue
|
||||
|
||||
# Strip 2, Channel 2 (Group 1)
|
||||
- platform: pyramidrgb
|
||||
id: rgb2_ch2_red
|
||||
pyramidrgb_id: pyramid_rgb2
|
||||
channel: 2
|
||||
color: red
|
||||
- platform: pyramidrgb
|
||||
id: rgb2_ch2_green
|
||||
pyramidrgb_id: pyramid_rgb2
|
||||
channel: 2
|
||||
color: green
|
||||
- platform: pyramidrgb
|
||||
id: rgb2_ch2_blue
|
||||
pyramidrgb_id: pyramid_rgb2
|
||||
channel: 2
|
||||
color: blue
|
||||
|
||||
# Strip 2, Channel 3 (Group 2)
|
||||
- platform: pyramidrgb
|
||||
id: rgb2_ch3_red
|
||||
pyramidrgb_id: pyramid_rgb2
|
||||
channel: 3
|
||||
color: red
|
||||
- platform: pyramidrgb
|
||||
id: rgb2_ch3_green
|
||||
pyramidrgb_id: pyramid_rgb2
|
||||
channel: 3
|
||||
color: green
|
||||
- platform: pyramidrgb
|
||||
id: rgb2_ch3_blue
|
||||
pyramidrgb_id: pyramid_rgb2
|
||||
channel: 3
|
||||
color: blue
|
||||
|
||||
light:
|
||||
- platform: monochromatic
|
||||
name: "LCD Backlight"
|
||||
output: lp5562_white_channel
|
||||
icon: "mdi:television"
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
- platform: rgb
|
||||
name: "Strip1 Group1"
|
||||
red: rgb1_ch0_red
|
||||
green: rgb1_ch0_green
|
||||
blue: rgb1_ch0_blue
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
|
||||
- platform: rgb
|
||||
name: "Strip1 Group2"
|
||||
red: rgb1_ch1_red
|
||||
green: rgb1_ch1_green
|
||||
blue: rgb1_ch1_blue
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
|
||||
- platform: rgb
|
||||
name: "Strip2 Group1"
|
||||
red: rgb2_ch2_red
|
||||
green: rgb2_ch2_green
|
||||
blue: rgb2_ch2_blue
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
|
||||
- platform: rgb
|
||||
name: "Strip2 Group2"
|
||||
red: rgb2_ch3_red
|
||||
green: rgb2_ch3_green
|
||||
blue: rgb2_ch3_blue
|
||||
restore_mode: RESTORE_DEFAULT_ON
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
id: atoms3r_lcd
|
||||
model: ST7789V
|
||||
dc_pin: GPIO42
|
||||
reset_pin: GPIO48
|
||||
cs_pin: GPIO14
|
||||
data_rate: 40MHz
|
||||
dimensions:
|
||||
height: 128
|
||||
width: 128
|
||||
offset_width: 2
|
||||
offset_height: 1
|
||||
|
||||
invert_colors: true
|
||||
rotation: 180°
|
||||
pages:
|
||||
- id: idle_page
|
||||
lambda: |-
|
||||
it.fill(id(idle_color));
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_idle), ImageAlign::CENTER);
|
||||
- id: listening_page
|
||||
lambda: |-
|
||||
it.fill(id(listening_color));
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_listening), ImageAlign::CENTER);
|
||||
- id: thinking_page
|
||||
lambda: |-
|
||||
it.fill(id(thinking_color));
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_thinking), ImageAlign::CENTER);
|
||||
- id: replying_page
|
||||
lambda: |-
|
||||
it.fill(id(replying_color));
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_replying), ImageAlign::CENTER);
|
||||
- id: error_page
|
||||
lambda: |-
|
||||
it.fill(id(error_color));
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_error), ImageAlign::CENTER);
|
||||
- id: no_ha_page
|
||||
lambda: |-
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(error_no_ha), ImageAlign::CENTER);
|
||||
- id: no_wifi_page
|
||||
lambda: |-
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(error_no_wifi), ImageAlign::CENTER);
|
||||
- id: initializing_page
|
||||
lambda: |-
|
||||
it.fill(id(loading_color));
|
||||
it.image((it.get_width() / 2), (it.get_height() / 2), id(casita_initializing), ImageAlign::CENTER);
|
||||
- id: muted_page
|
||||
lambda: |-
|
||||
it.fill(Color::BLACK);
|
||||
it.printf(0, 0, id(mdi_icon_128), Color::WHITE, "%s", "\U000F036D");
|
||||
|
||||
script:
|
||||
# Starts either mWW or the streaming wake word, depending on the configured location
|
||||
- id: start_wake_word
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
and:
|
||||
- not:
|
||||
- voice_assistant.is_running:
|
||||
- lambda: |-
|
||||
return id(wake_word_engine_location).current_option() == "On device";
|
||||
then:
|
||||
- lambda: id(va).set_use_wake_word(false);
|
||||
- micro_wake_word.start:
|
||||
- if:
|
||||
condition:
|
||||
and:
|
||||
- not:
|
||||
- voice_assistant.is_running:
|
||||
- lambda: |-
|
||||
return id(wake_word_engine_location).current_option() == "In Home Assistant";
|
||||
then:
|
||||
- lambda: id(va).set_use_wake_word(true);
|
||||
- voice_assistant.start_continuous:
|
||||
# Stops either mWW or the streaming wake word, depending on the configured location
|
||||
- id: stop_wake_word
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: |-
|
||||
return id(wake_word_engine_location).current_option() == "In Home Assistant";
|
||||
then:
|
||||
- lambda: id(va).set_use_wake_word(false);
|
||||
- voice_assistant.stop:
|
||||
- if:
|
||||
condition:
|
||||
lambda: |-
|
||||
return id(wake_word_engine_location).current_option() == "On device";
|
||||
then:
|
||||
- micro_wake_word.stop:
|
||||
# Set the voice assistant phase to idle or muted, depending on if the software mute switch is activated
|
||||
- id: set_idle_or_mute_phase
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
switch.is_off: mute
|
||||
then:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_idle_phase_id};
|
||||
else:
|
||||
- lambda: id(voice_assistant_phase) = ${voice_assist_muted_phase_id};
|
||||
|
||||
- id: play_sound
|
||||
parameters:
|
||||
priority: bool
|
||||
sound_file: "audio::AudioFile*"
|
||||
then:
|
||||
- lambda: |-
|
||||
if (priority) {
|
||||
id(echo_pyramid_player)
|
||||
->make_call()
|
||||
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_STOP)
|
||||
.set_announcement(true)
|
||||
.perform();
|
||||
}
|
||||
if ( (id(echo_pyramid_player).state != media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING ) || priority) {
|
||||
id(echo_pyramid_player)
|
||||
->play_file(sound_file, true, false);
|
||||
}
|
||||
|
||||
- id: draw_display
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return !id(init_in_progress);
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
wifi.connected:
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
api.connected:
|
||||
then:
|
||||
- lambda: |
|
||||
switch(id(voice_assistant_phase)) {
|
||||
case ${voice_assist_listening_phase_id}:
|
||||
id(atoms3r_lcd).show_page(listening_page);
|
||||
id(atoms3r_lcd).update();
|
||||
break;
|
||||
case ${voice_assist_thinking_phase_id}:
|
||||
id(atoms3r_lcd).show_page(thinking_page);
|
||||
id(atoms3r_lcd).update();
|
||||
break;
|
||||
case ${voice_assist_replying_phase_id}:
|
||||
id(atoms3r_lcd).show_page(replying_page);
|
||||
id(atoms3r_lcd).update();
|
||||
break;
|
||||
case ${voice_assist_error_phase_id}:
|
||||
id(atoms3r_lcd).show_page(error_page);
|
||||
id(atoms3r_lcd).update();
|
||||
break;
|
||||
case ${voice_assist_muted_phase_id}:
|
||||
id(atoms3r_lcd).show_page(muted_page);
|
||||
id(atoms3r_lcd).update();
|
||||
break;
|
||||
case ${voice_assist_not_ready_phase_id}:
|
||||
id(atoms3r_lcd).show_page(no_ha_page);
|
||||
id(atoms3r_lcd).update();
|
||||
break;
|
||||
default:
|
||||
id(atoms3r_lcd).show_page(idle_page);
|
||||
id(atoms3r_lcd).update();
|
||||
}
|
||||
else:
|
||||
- display.page.show: no_ha_page
|
||||
- component.update: atoms3r_lcd
|
||||
else:
|
||||
- display.page.show: no_wifi_page
|
||||
- component.update: atoms3r_lcd
|
||||
else:
|
||||
- display.page.show: initializing_page
|
||||
- component.update: atoms3r_lcd
|
||||
|
||||
image:
|
||||
- file: ${error_illustration_file}
|
||||
id: casita_error
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
- file: ${idle_illustration_file}
|
||||
id: casita_idle
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
- file: ${listening_illustration_file}
|
||||
id: casita_listening
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
- file: ${thinking_illustration_file}
|
||||
id: casita_thinking
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
- file: ${replying_illustration_file}
|
||||
id: casita_replying
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
- file: ${loading_illustration_file}
|
||||
id: casita_initializing
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
- file: ${error_no_wifi_illustration_file}
|
||||
id: error_no_wifi
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
- file: ${error_no_ha_illustration_file}
|
||||
id: error_no_ha
|
||||
resize: 160x120
|
||||
type: RGB
|
||||
transparency: alpha_channel
|
||||
|
||||
font:
|
||||
- file: ${mdi_webfont_file}
|
||||
id: mdi_icon_128
|
||||
size: 128
|
||||
bpp: 4
|
||||
glyphs:
|
||||
- "\U000F036D" # mdi:mic-mute
|
||||
|
||||
color:
|
||||
- id: idle_color
|
||||
hex: ${idle_illustration_background_color}
|
||||
- id: listening_color
|
||||
hex: ${listening_illustration_background_color}
|
||||
- id: thinking_color
|
||||
hex: ${thinking_illustration_background_color}
|
||||
- id: replying_color
|
||||
hex: ${replying_illustration_background_color}
|
||||
- id: loading_color
|
||||
hex: ${loading_illustration_background_color}
|
||||
- id: error_color
|
||||
hex: ${error_illustration_background_color}
|
||||
599
active/device_esphome/tab1.yaml
Normal file
@@ -0,0 +1,599 @@
|
||||
esphome:
|
||||
name: tab1
|
||||
friendly_name: M5Stack Tab5 1
|
||||
on_boot:
|
||||
# Set the charging icon to the correct state on boot
|
||||
- then:
|
||||
- logger.log: "Delaying backlight initialization"
|
||||
- delay: 2s
|
||||
- logger.log: "End delay"
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(charging).state;
|
||||
then:
|
||||
- lvgl.widget.show:
|
||||
id: charging_icon_widget
|
||||
else:
|
||||
- lvgl.widget.hide:
|
||||
id: charging_icon_widget
|
||||
|
||||
esp32:
|
||||
board: esp32-p4-evboard
|
||||
flash_size: 16MB
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
enable_idf_experimental_features: true
|
||||
|
||||
esp32_hosted:
|
||||
variant: esp32c6
|
||||
active_high: true
|
||||
clk_pin: GPIO12
|
||||
cmd_pin: GPIO13
|
||||
d0_pin: GPIO11
|
||||
d1_pin: GPIO10
|
||||
d2_pin: GPIO9
|
||||
d3_pin: GPIO8
|
||||
reset_pin: GPIO15
|
||||
slot: 1
|
||||
|
||||
logger:
|
||||
hardware_uart: USB_SERIAL_JTAG
|
||||
level: DEBUG
|
||||
|
||||
psram:
|
||||
mode: hex
|
||||
speed: 200MHz
|
||||
|
||||
api:
|
||||
|
||||
# Touchscreen support
|
||||
external_components:
|
||||
- source: github://pr#12075
|
||||
components: [st7123]
|
||||
refresh: 1h
|
||||
|
||||
ota:
|
||||
platform: esphome
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
fast_connect: true
|
||||
on_connect:
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
text: "IDLE"
|
||||
- select.set:
|
||||
id: dac_output
|
||||
option: "LINE1"
|
||||
- lvgl.label.update:
|
||||
id: lbl_ip
|
||||
text: !lambda return id(ip_addr).state;
|
||||
- lvgl.label.update:
|
||||
id: lbl_ap
|
||||
text: !lambda return id(ssid).state;
|
||||
on_disconnect:
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
text: "DISCONNECTED"
|
||||
|
||||
text_sensor:
|
||||
- platform: wifi_info
|
||||
ip_address:
|
||||
id: ip_addr
|
||||
name: Device IP Address
|
||||
address_0:
|
||||
name: Device IP Address 0
|
||||
address_1:
|
||||
name: Device IP Address 1
|
||||
address_2:
|
||||
name: Device IP Address 2
|
||||
address_3:
|
||||
name: Device IP Address 3
|
||||
address_4:
|
||||
name: Device IP Address 4
|
||||
ssid:
|
||||
id: ssid
|
||||
name: Device Connected SSID
|
||||
bssid:
|
||||
name: Device Connected BSSID
|
||||
mac_address:
|
||||
name: Device Mac Wifi Address
|
||||
scan_results:
|
||||
name: Device Latest Scan Results
|
||||
dns_address:
|
||||
name: Device DNS Address
|
||||
power_save_mode:
|
||||
name: Device Wifi Power Save Mode
|
||||
|
||||
time:
|
||||
- platform: sntp
|
||||
id: sntp_time
|
||||
timezone: America/New_York
|
||||
servers:
|
||||
- 0.pool.ntp.org
|
||||
- 1.pool.ntp.org
|
||||
- 2.pool.ntp.org
|
||||
|
||||
# wireguard:
|
||||
# address: !secret tab1_wg_ip
|
||||
# private_key: !secret tab1_wg_pk
|
||||
# peer_endpoint: !secret wg_host
|
||||
# peer_public_key: !secret wg_pubkey
|
||||
|
||||
# # Optional keepalive (disabled by default)
|
||||
# peer_persistent_keepalive: 25s
|
||||
|
||||
i2c:
|
||||
- id: bsp_bus
|
||||
sda: GPIO31
|
||||
scl: GPIO32
|
||||
frequency: 400kHz
|
||||
|
||||
pi4ioe5v6408:
|
||||
- id: pi4ioe1
|
||||
address: 0x43
|
||||
# 0: O - wifi_antenna_int_ext
|
||||
# 1: O - speaker_enable
|
||||
# 2: O - external_5v_power
|
||||
# 3: NC
|
||||
# 4: O - lcd reset
|
||||
# 5: O - touch panel reset
|
||||
# 6: O - camera reset
|
||||
# 7: I - headphone detect
|
||||
- id: pi4ioe2
|
||||
address: 0x44
|
||||
# 0: O - wifi_power
|
||||
# 1: NC
|
||||
# 2: NC
|
||||
# 3: O - usb_5v_power
|
||||
# 4: O - poweroff pulse
|
||||
# 5: O - quick charge enable (inverted)
|
||||
# 6: I - charging status
|
||||
# 7: O - charge enable
|
||||
|
||||
button:
|
||||
- platform: restart
|
||||
name: "Restart Tablet"
|
||||
|
||||
switch:
|
||||
- platform: gpio
|
||||
id: wifi_power
|
||||
name: "WiFi Power"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe2
|
||||
number: 0
|
||||
restore_mode: ALWAYS_ON
|
||||
- platform: gpio
|
||||
id: usb_5v_power
|
||||
name: "USB Power"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe2
|
||||
number: 3
|
||||
- platform: gpio
|
||||
id: quick_charge
|
||||
name: "Quick Charge"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe2
|
||||
number: 5
|
||||
inverted: true
|
||||
- platform: gpio
|
||||
id: charge_enable
|
||||
name: "Charge Enable"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe2
|
||||
number: 7
|
||||
restore_mode: ALWAYS_ON
|
||||
- platform: gpio
|
||||
id: wifi_antenna_int_ext
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe1
|
||||
number: 0
|
||||
- platform: gpio
|
||||
id: speaker_enable
|
||||
name: "Speaker Enable"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe1
|
||||
number: 1
|
||||
restore_mode: ALWAYS_ON
|
||||
- platform: gpio
|
||||
id: external_5v_power
|
||||
name: "External 5V Power"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe1
|
||||
number: 2
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
id: charging
|
||||
name: "Charging Status"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe2
|
||||
number: 6
|
||||
mode: INPUT_PULLDOWN
|
||||
on_state:
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(charging).state;
|
||||
then:
|
||||
- lvgl.widget.show:
|
||||
id: charging_icon_widget
|
||||
else:
|
||||
- lvgl.widget.hide:
|
||||
id: charging_icon_widget
|
||||
|
||||
- platform: gpio
|
||||
id: headphone_detect
|
||||
name: "Headphone Detect"
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe1
|
||||
number: 7
|
||||
- platform: lvgl
|
||||
widget: volume_up_widget
|
||||
name: Volume Up Button
|
||||
on_press:
|
||||
then:
|
||||
- logger.log: "Button pressed"
|
||||
- media_player.volume_up:
|
||||
id: tab5_media_player
|
||||
- delay: 100ms
|
||||
- lvgl.label.update:
|
||||
id: lbl_volume
|
||||
text: !lambda return to_string(int(id(tab5_media_player).volume * 100));
|
||||
- light.turn_on:
|
||||
id: backlight
|
||||
brightness: !lambda |-
|
||||
float current_value = id(backlight).current_values.get_brightness();
|
||||
return current_value < 0.6 ? 0.6 : current_value + 0.2;
|
||||
- platform: lvgl
|
||||
widget: volume_down_widget
|
||||
name: Volume Down Button
|
||||
on_press:
|
||||
then:
|
||||
- logger.log: "Button pressed"
|
||||
- media_player.volume_down:
|
||||
id: tab5_media_player
|
||||
- lvgl.label.update:
|
||||
id: lbl_volume
|
||||
text: !lambda return to_string(int(id(tab5_media_player).volume * 100));
|
||||
- light.turn_on:
|
||||
id: backlight
|
||||
brightness: !lambda |-
|
||||
float current_value = id(backlight).current_values.get_brightness();
|
||||
return current_value < 0.6 ? 0.6 : current_value - 0.2;
|
||||
|
||||
sensor:
|
||||
- platform: ina226
|
||||
address: 0x41
|
||||
adc_averaging: 16
|
||||
max_current: 8.192A
|
||||
shunt_resistance: 0.005ohm
|
||||
bus_voltage:
|
||||
id: battery_voltage
|
||||
name: "Battery Voltage"
|
||||
current:
|
||||
id: battery_current
|
||||
name: "Battery Current"
|
||||
# Positive means discharging
|
||||
# Negative means charging
|
||||
|
||||
# Tab5 built-in battery discharges from full (8.23 V) to shutdown threshold (6.0 V)
|
||||
- platform: template
|
||||
name: "Battery Percentage"
|
||||
lambda: |-
|
||||
float voltage = id(battery_voltage).state;
|
||||
// Adjust these values based on your battery's actual min/max voltage
|
||||
float min_voltage = 6.75; // Discharged voltage
|
||||
float max_voltage = 8.2; // Fully charged voltage
|
||||
float percentage = (voltage - min_voltage) / (max_voltage - min_voltage) * 100.0;
|
||||
if (percentage > 100.0) return 100.0;
|
||||
if (percentage < 0.0) return 0.0;
|
||||
return percentage;
|
||||
update_interval: 60s
|
||||
unit_of_measurement: "%"
|
||||
accuracy_decimals: 1
|
||||
id: battery_percent
|
||||
on_value:
|
||||
then:
|
||||
- lvgl.label.update:
|
||||
id: lbl_battery
|
||||
text:
|
||||
format: "Battery: %.1f%%"
|
||||
args: ["id(battery_percent).state"]
|
||||
|
||||
touchscreen:
|
||||
- platform: st7123
|
||||
i2c_id: bsp_bus
|
||||
interrupt_pin: GPIO23
|
||||
display: lcd
|
||||
update_interval: never
|
||||
reset_pin:
|
||||
pi4ioe5v6408: pi4ioe1
|
||||
number: 5
|
||||
calibration:
|
||||
x_min: 0
|
||||
x_max: 720
|
||||
y_min: 0
|
||||
y_max: 1280
|
||||
id: touch
|
||||
on_touch:
|
||||
- logger.log: "LVGL resuming"
|
||||
- lvgl.resume:
|
||||
- light.turn_on: backlight
|
||||
# on_release:
|
||||
# - media_player.stop:
|
||||
|
||||
esp_ldo:
|
||||
- voltage: 2.5V
|
||||
channel: 3
|
||||
|
||||
display:
|
||||
- platform: mipi_dsi
|
||||
id: lcd
|
||||
dimensions:
|
||||
height: 1280
|
||||
width: 720
|
||||
model: M5STACK-TAB5-V2
|
||||
reset_pin:
|
||||
pi4ioe5v6408: pi4ioe1
|
||||
number: 4
|
||||
|
||||
output:
|
||||
- platform: ledc
|
||||
pin: GPIO22
|
||||
id: backlight_pwm
|
||||
frequency: 1000Hz
|
||||
|
||||
light:
|
||||
- platform: monochromatic
|
||||
output: backlight_pwm
|
||||
name: "Display Backlight"
|
||||
id: backlight
|
||||
restore_mode: ALWAYS_ON
|
||||
default_transition_length: 250ms
|
||||
initial_state:
|
||||
brightness: "50%"
|
||||
|
||||
image:
|
||||
defaults:
|
||||
type: rgb565
|
||||
transparency: alpha_channel
|
||||
resize: 512x512
|
||||
byte_order: little_endian
|
||||
images:
|
||||
- file: "images/va_idle.png"
|
||||
id: va_idle
|
||||
- file: "images/va_listen.png"
|
||||
id: va_listen
|
||||
- file: "images/va_speak.png"
|
||||
id: va_speak
|
||||
- file: "images/charging.png"
|
||||
id: charging_icon
|
||||
resize: 64x64
|
||||
|
||||
lvgl:
|
||||
byte_order: little_endian
|
||||
|
||||
on_idle:
|
||||
timeout: 120s
|
||||
then:
|
||||
- logger.log: "LVGL is idle"
|
||||
- light.turn_off:
|
||||
id: backlight
|
||||
transition_length: 15s
|
||||
- lvgl.pause:
|
||||
widgets:
|
||||
- obj:
|
||||
align: TOP_MID
|
||||
width: 100%
|
||||
height: 100%
|
||||
layout:
|
||||
type: flex
|
||||
flex_flow: column
|
||||
flex_align_main: START
|
||||
flex_align_track: center
|
||||
flex_align_cross: center
|
||||
widgets:
|
||||
- label:
|
||||
align: TOP_MID
|
||||
id: lbl_status
|
||||
text_font: montserrat_48
|
||||
text: "CONNECTING..."
|
||||
- label:
|
||||
align: TOP_MID
|
||||
id: lbl_ap
|
||||
text_font: montserrat_22
|
||||
text: "CONNECTING..."
|
||||
- label:
|
||||
align: TOP_MID
|
||||
id: lbl_ip
|
||||
text_font: montserrat_22
|
||||
text: "CONNECTING..."
|
||||
- image:
|
||||
id: listen_icon_widget
|
||||
src: va_idle
|
||||
align: CENTER
|
||||
- label:
|
||||
align: BOTTOM_LEFT
|
||||
id: lbl_version
|
||||
text_font: montserrat_12
|
||||
text: "v0.6"
|
||||
- label:
|
||||
align: BOTTOM_RIGHT
|
||||
id: lbl_battery
|
||||
text_font: montserrat_28
|
||||
text: Loading...
|
||||
- image:
|
||||
id: charging_icon_widget
|
||||
src: charging_icon
|
||||
align: TOP_RIGHT
|
||||
- button:
|
||||
id: volume_up_widget
|
||||
widgets:
|
||||
- label:
|
||||
text: "\uF028"
|
||||
text_font: montserrat_48
|
||||
text_align: CENTER
|
||||
align: CENTER
|
||||
x: 20
|
||||
y: 20
|
||||
width: 100
|
||||
height: 100
|
||||
pad_all: 8
|
||||
- button:
|
||||
id: volume_down_widget
|
||||
widgets:
|
||||
- label:
|
||||
text: "\uF027"
|
||||
text_font: montserrat_48
|
||||
text_align: CENTER
|
||||
align: CENTER
|
||||
x: 20
|
||||
y: 140
|
||||
width: 100
|
||||
height: 100
|
||||
pad_all: 8
|
||||
- label:
|
||||
x: 20
|
||||
y: 260
|
||||
id: lbl_volume
|
||||
text_font: montserrat_28
|
||||
text: !lambda return "%.1f",to_string(id(tab5_media_player).volume);
|
||||
|
||||
# The DAC Output select needs to be manually (or with an automation) changed to `LINE1` for the onboard speaker
|
||||
select:
|
||||
- platform: es8388
|
||||
dac_output:
|
||||
name: DAC Output
|
||||
id: dac_output
|
||||
adc_input_mic:
|
||||
name: ADC Input Mic
|
||||
id: adc_input
|
||||
|
||||
- platform: template
|
||||
id: wifi_antenna_select
|
||||
name: "WiFi Antenna"
|
||||
options:
|
||||
- "Internal"
|
||||
- "External"
|
||||
optimistic: true
|
||||
on_value:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return i == 0;
|
||||
then:
|
||||
- switch.turn_off: wifi_antenna_int_ext
|
||||
else:
|
||||
- switch.turn_on: wifi_antenna_int_ext
|
||||
|
||||
i2s_audio:
|
||||
- id: mic_bus
|
||||
i2s_lrclk_pin: GPIO29
|
||||
i2s_bclk_pin: GPIO27
|
||||
i2s_mclk_pin: GPIO30
|
||||
|
||||
audio_adc:
|
||||
- platform: es7210
|
||||
id: es7210_adc
|
||||
bits_per_sample: 16bit
|
||||
sample_rate: 16000
|
||||
|
||||
microphone:
|
||||
- platform: i2s_audio
|
||||
id: tab5_microphone
|
||||
i2s_din_pin: GPIO28
|
||||
sample_rate: 16000
|
||||
bits_per_sample: 16bit
|
||||
adc_type: external
|
||||
|
||||
audio_dac:
|
||||
- platform: es8388
|
||||
id: es8388_dac
|
||||
|
||||
speaker:
|
||||
- platform: i2s_audio
|
||||
id: tab5_speaker
|
||||
i2s_dout_pin: GPIO26
|
||||
audio_dac: es8388_dac
|
||||
dac_type: external
|
||||
channel: mono
|
||||
buffer_duration: 100ms
|
||||
bits_per_sample: 16bit
|
||||
sample_rate: 48000
|
||||
|
||||
media_player:
|
||||
- platform: speaker
|
||||
name: None
|
||||
id: tab5_media_player
|
||||
announcement_pipeline:
|
||||
speaker: tab5_speaker
|
||||
format: WAV
|
||||
|
||||
micro_wake_word:
|
||||
id: mww
|
||||
models:
|
||||
- okay_nabu
|
||||
- hey_mycroft
|
||||
- hey_jarvis
|
||||
on_wake_word_detected:
|
||||
- voice_assistant.start:
|
||||
wake_word: !lambda return wake_word;
|
||||
|
||||
voice_assistant:
|
||||
id: va
|
||||
microphone: tab5_microphone
|
||||
media_player: tab5_media_player
|
||||
micro_wake_word: mww
|
||||
on_listening:
|
||||
- logger.log: "LVGL resuming"
|
||||
- lvgl.resume:
|
||||
- light.turn_on: backlight
|
||||
- lvgl.image.update:
|
||||
id: listen_icon_widget
|
||||
src: va_listen
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
text: "LISTENING"
|
||||
on_stt_vad_end:
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
text: "PROCESSING"
|
||||
- lvgl.image.update:
|
||||
id: listen_icon_widget
|
||||
src: va_idle
|
||||
on_tts_start:
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
text: "RESPONDING"
|
||||
- lvgl.image.update:
|
||||
id: listen_icon_widget
|
||||
src: va_speak
|
||||
on_end:
|
||||
# Wait a short amount of time to see if an announcement starts
|
||||
- wait_until:
|
||||
condition:
|
||||
- media_player.is_announcing:
|
||||
timeout: 0.5s
|
||||
# Announcement is finished and the I2S bus is free
|
||||
- wait_until:
|
||||
- and:
|
||||
- not:
|
||||
media_player.is_announcing:
|
||||
- not:
|
||||
speaker.is_playing:
|
||||
- micro_wake_word.start:
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
text: "IDLE"
|
||||
- lvgl.image.update:
|
||||
id: listen_icon_widget
|
||||
src: va_idle
|
||||
- light.turn_off:
|
||||
id: backlight
|
||||
transition_length: 15s
|
||||
on_client_connected:
|
||||
- micro_wake_word.start:
|
||||
on_client_disconnected:
|
||||
- micro_wake_word.stop:
|
||||
@@ -1,6 +1,18 @@
|
||||
esphome:
|
||||
name: tab1
|
||||
friendly_name: M5Stack Tab5 1
|
||||
name: tab2
|
||||
friendly_name: M5Stack Tab5 2
|
||||
on_boot:
|
||||
# Set the charing icon to the correct state on boot
|
||||
- then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(charging).state;
|
||||
then:
|
||||
- lvgl.widget.show:
|
||||
id: charging_icon_widget
|
||||
else:
|
||||
- lvgl.widget.hide:
|
||||
id: charging_icon_widget
|
||||
|
||||
esp32:
|
||||
board: esp32-p4-evboard
|
||||
@@ -43,6 +55,7 @@ ota:
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
fast_connect: true
|
||||
on_connect:
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
@@ -54,6 +67,29 @@ wifi:
|
||||
- lvgl.label.update:
|
||||
id: lbl_status
|
||||
text: "DISCONNECTED"
|
||||
# ap:
|
||||
# password: !secret hotspot_password
|
||||
# ap_timeout: 90s
|
||||
|
||||
# captive_portal:
|
||||
|
||||
time:
|
||||
- platform: sntp
|
||||
id: sntp_time
|
||||
timezone: America/New_York
|
||||
servers:
|
||||
- 0.pool.ntp.org
|
||||
- 1.pool.ntp.org
|
||||
- 2.pool.ntp.org
|
||||
|
||||
# wireguard:
|
||||
# address: !secret tab1_wg_ip
|
||||
# private_key: !secret tab1_wg_pk
|
||||
# peer_endpoint: !secret wg_host
|
||||
# peer_public_key: !secret wg_pubkey
|
||||
|
||||
# # Optional keepalive (disabled by default)
|
||||
# peer_persistent_keepalive: 25s
|
||||
|
||||
i2c:
|
||||
- id: bsp_bus
|
||||
@@ -142,6 +178,17 @@ binary_sensor:
|
||||
pi4ioe5v6408: pi4ioe2
|
||||
number: 6
|
||||
mode: INPUT_PULLDOWN
|
||||
on_state:
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
lambda: return id(charging).state;
|
||||
then:
|
||||
- lvgl.widget.show:
|
||||
id: charging_icon_widget
|
||||
else:
|
||||
- lvgl.widget.hide:
|
||||
id: charging_icon_widget
|
||||
|
||||
- platform: gpio
|
||||
id: headphone_detect
|
||||
@@ -186,7 +233,7 @@ sensor:
|
||||
- lvgl.label.update:
|
||||
id: lbl_battery
|
||||
text:
|
||||
format: "Battery: %.1f%"
|
||||
format: "Battery: %.1f%%"
|
||||
args: ["id(battery_percent).state"]
|
||||
|
||||
touchscreen:
|
||||
@@ -255,6 +302,9 @@ image:
|
||||
id: va_listen
|
||||
- file: "images/va_speak.png"
|
||||
id: va_speak
|
||||
- file: "images/charging.png"
|
||||
id: charging_icon
|
||||
resize: 64x64
|
||||
|
||||
lvgl:
|
||||
byte_order: little_endian
|
||||
@@ -287,6 +337,25 @@ lvgl:
|
||||
id: lbl_battery
|
||||
text_font: montserrat_28
|
||||
text: Loading...
|
||||
- image:
|
||||
id: charging_icon_widget
|
||||
src: charging_icon
|
||||
align: TOP_RIGHT
|
||||
- slider:
|
||||
id: backlight_slider
|
||||
x: 20
|
||||
y: 50
|
||||
width: 30
|
||||
height: 220
|
||||
pad_all: 8
|
||||
min_value: 0
|
||||
max_value: 255
|
||||
on_release:
|
||||
- homeassistant.action:
|
||||
action: light.turn_on
|
||||
data:
|
||||
entity_id: light.backlight
|
||||
brightness: !lambda return int(x);
|
||||
|
||||
# The DAC Output select needs to be manually (or with an automation) changed to `LINE1` for the onboard speaker
|
||||
select:
|
||||
13
active/device_unifi/config.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
rabbitmq:
|
||||
host: "rabbitmq.reeselink.com"
|
||||
port: 5672
|
||||
virtual_host: "/"
|
||||
username: "user"
|
||||
password: "password"
|
||||
|
||||
# Which *exchange* (topic) you actually want to listen to.
|
||||
# The program will create a temporary queue, bind it to this exchange
|
||||
# with the routing key supplied in `routing_key`.
|
||||
subscriber:
|
||||
exchange: "nic" # ← change to “reese” or any other exchange
|
||||
routing_key: "add" # ← could be “add”, “delete”, or any pattern
|
||||
136
active/device_unifi/rmq_dns.py
Normal file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
RabbitMQ setup & consumer using pika.
|
||||
|
||||
- Creates two **topic exchanges**: nic, reese
|
||||
- For each exchange creates two queues: add, delete
|
||||
- Binds the queues with routing keys “add” and “delete”
|
||||
- Subscribes (consumes) from a **single** exchange/queue pair that is
|
||||
supplied via a tiny config file (config.yaml).
|
||||
|
||||
Run:
|
||||
python3 rabbit_demo.py
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pika # type: ignore
|
||||
import yaml
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 1️⃣ Load configuration
|
||||
# ----------------------------------------------------------------------
|
||||
DEFAULT_CFG = """
|
||||
rabbitmq:
|
||||
host: "localhost"
|
||||
port: 5672
|
||||
virtual_host: "/"
|
||||
username: "guest"
|
||||
password: "guest"
|
||||
|
||||
# Which *exchange* (topic) you actually want to listen to.
|
||||
# The program will create a temporary queue, bind it to this exchange
|
||||
# with the routing key supplied in `routing_key`.
|
||||
subscriber:
|
||||
exchange: "nic" # ← change to “reese” or any other exchange
|
||||
routing_key: "add" # ← could be “add”, “delete”, or any pattern
|
||||
"""
|
||||
|
||||
CONFIG_PATH = Path("active/device_unifi/config.yaml")
|
||||
if not CONFIG_PATH.exists():
|
||||
CONFIG_PATH.write_text(DEFAULT_CFG)
|
||||
|
||||
with CONFIG_PATH.open() as f:
|
||||
cfg = yaml.safe_load(f)
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 2️⃣ Build connection parameters
|
||||
# ----------------------------------------------------------------------
|
||||
cred = pika.PlainCredentials(cfg["rabbitmq"]["username"], cfg["rabbitmq"]["password"])
|
||||
params = pika.ConnectionParameters(
|
||||
host=cfg["rabbitmq"]["host"],
|
||||
port=cfg["rabbitmq"]["port"],
|
||||
virtual_host=cfg["rabbitmq"]["virtual_host"],
|
||||
credentials=cred,
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 3️⃣ Helper to declare exchanges / queues
|
||||
# ----------------------------------------------------------------------
|
||||
def declare_topology(channel):
|
||||
"""
|
||||
Create the two topic exchanges and the four queues,
|
||||
then bind each queue to its exchange with the appropriate routing key.
|
||||
"""
|
||||
exchanges = ["nic", "reese"]
|
||||
routing_keys = ["add", "delete"]
|
||||
|
||||
for exch in exchanges:
|
||||
channel.exchange_declare(exchange=exch, exchange_type="topic", durable=True)
|
||||
for key in routing_keys:
|
||||
queue_name = f"{exch}_{key}" # e.g. nic_add, reese_delete
|
||||
channel.queue_declare(queue=queue_name, durable=True)
|
||||
# bind queue to the exchange with the same routing key
|
||||
channel.queue_bind(queue=queue_name, exchange=exch, routing_key=key)
|
||||
logging.info(
|
||||
f"Declared queue {queue_name} bound to {exch} with key '{key}'"
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 4️⃣ Consumer callback
|
||||
# ----------------------------------------------------------------------
|
||||
def on_message(ch, method, properties, body):
|
||||
logging.info(
|
||||
f"Received from exchange '{method.exchange}' "
|
||||
f"routing_key='{method.routing_key}': {body!r}"
|
||||
)
|
||||
# Acknowledge the message
|
||||
ch.basic_ack(delivery_tag=method.delivery_tag)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 5️⃣ Main routine
|
||||
# ----------------------------------------------------------------------
|
||||
def main():
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s"
|
||||
)
|
||||
|
||||
with pika.BlockingConnection(params) as conn:
|
||||
channel = conn.channel()
|
||||
|
||||
# 1️⃣ Declare the static topology (exchanges + queues)
|
||||
declare_topology(channel)
|
||||
|
||||
# 2️⃣ Set up a *temporary* queue for the subscriber defined in config
|
||||
result = channel.queue_declare(
|
||||
queue="", exclusive=True
|
||||
) # server‑generated name
|
||||
tmp_queue = result.method.queue
|
||||
|
||||
exch = cfg["subscriber"]["exchange"]
|
||||
rkey = cfg["subscriber"]["routing_key"]
|
||||
channel.queue_bind(queue=tmp_queue, exchange=exch, routing_key=rkey)
|
||||
|
||||
logging.info(
|
||||
f"Subscribed to exchange '{exch}' with routing_key '{rkey}' "
|
||||
f"using temporary queue '{tmp_queue}'"
|
||||
)
|
||||
|
||||
# 3️⃣ Start consuming
|
||||
channel.basic_consume(queue=tmp_queue, on_message_callback=on_message)
|
||||
|
||||
try:
|
||||
logging.info("Waiting for messages. Press Ctrl+C to exit.")
|
||||
channel.start_consuming()
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Interrupted – closing connection.")
|
||||
channel.stop_consuming()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
46
active/device_unifi/rmq_dns_test.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pika
|
||||
import yaml
|
||||
|
||||
DEFAULT_CFG = """
|
||||
rabbitmq:
|
||||
host: "localhost"
|
||||
port: 5672
|
||||
virtual_host: "/"
|
||||
username: "guest"
|
||||
password: "guest"
|
||||
|
||||
# Which *exchange* (topic) you actually want to listen to.
|
||||
# The program will create a temporary queue, bind it to this exchange
|
||||
# with the routing key supplied in `routing_key`.
|
||||
subscriber:
|
||||
exchange: "nic" # ← change to “reese” or any other exchange
|
||||
routing_key: "add" # ← could be “add”, “delete”, or any pattern
|
||||
"""
|
||||
|
||||
CONFIG_PATH = Path("active/device_unifi/config.yaml")
|
||||
if not CONFIG_PATH.exists():
|
||||
CONFIG_PATH.write_text(DEFAULT_CFG)
|
||||
|
||||
with CONFIG_PATH.open() as f:
|
||||
cfg = yaml.safe_load(f)
|
||||
|
||||
|
||||
cred = pika.PlainCredentials(cfg["rabbitmq"]["username"], cfg["rabbitmq"]["password"])
|
||||
params = pika.ConnectionParameters(
|
||||
host=cfg["rabbitmq"]["host"],
|
||||
port=cfg["rabbitmq"]["port"],
|
||||
virtual_host=cfg["rabbitmq"]["virtual_host"],
|
||||
credentials=cred,
|
||||
)
|
||||
|
||||
with pika.BlockingConnection(params) as c:
|
||||
ch = c.channel()
|
||||
ch.basic_publish(
|
||||
exchange="reese",
|
||||
routing_key="add",
|
||||
body=json.dumps({"msg": "hello nic add"}),
|
||||
properties=pika.BasicProperties(delivery_mode=2), # make it persistent
|
||||
)
|
||||
15
active/device_unifi/test_update_dns.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from update_dns import ApiHelperMethods, ApiPaths, ApiWrapper
|
||||
|
||||
|
||||
def test_api_get():
|
||||
assert ApiWrapper.api_get(ApiPaths.list_sites()) is not None
|
||||
|
||||
|
||||
def test_site_name_to_id():
|
||||
assert ApiHelperMethods.site_name_to_id("Default") is not None
|
||||
|
||||
|
||||
def test_dns_record_exists():
|
||||
assert ApiHelperMethods.dns_record_exists("test.reeselink.com")
|
||||
|
||||
assert not ApiHelperMethods.dns_record_exists("idontexist.reeselink.com")
|
||||
58
active/device_unifi/unifi.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Unifi
|
||||
|
||||
## Update DNS Records via API
|
||||
|
||||
```bash
|
||||
export API_KEY=$(cat active/device_unifi/secrets/api-key)
|
||||
|
||||
# List site IDs
|
||||
curl -L -g -k -s "https://192.168.1.1/proxy/network/integration/v1/sites" \
|
||||
-H "Accept: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}" | jq -rc '.data[0].id'
|
||||
|
||||
# List domains
|
||||
curl -L -g -k -s "https://192.168.1.1/proxy/network/integration/v1/sites/88f7af54-98f8-306a-a1c7-c9349722b1f6/dns/policies" \
|
||||
-H "Accept: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}" | jq -r '.data[] | {domain, id}'
|
||||
|
||||
# List device domains
|
||||
curl -L -g -k -s "https://10.1.0.1/proxy/network/v2/api/site/default/static-dns/devices" \
|
||||
-H "Accept: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}" | jq -r
|
||||
|
||||
# List clients
|
||||
curl -L -g -k -s "https://192.168.1.1/proxy/network/integration/v1/sites/88f7af54-98f8-306a-a1c7-c9349722b1f6/clients" \
|
||||
-H "Accept: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}"
|
||||
|
||||
# List firewall policies
|
||||
curl -L -g -k -s "https://192.168.1.1/proxy/network/integration/v1/sites/88f7af54-98f8-306a-a1c7-c9349722b1f6/firewall/policies" \
|
||||
-H "Accept: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}"
|
||||
|
||||
# Create a record
|
||||
curl -L -g -k -s "https://192.168.1.1/proxy/network/integration/v1/sites/88f7af54-98f8-306a-a1c7-c9349722b1f6/dns/policies" \
|
||||
-H "Accept: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"type\": \"A_RECORD\",
|
||||
\"enabled\": true,
|
||||
\"domain\": \"test.reeselink.com\",
|
||||
\"ipv4Address\": \"10.1.0.100\",
|
||||
\"ttlSeconds\": 300
|
||||
}"
|
||||
|
||||
# Update a record
|
||||
curl -L -g -k -s -X PUT "https://192.168.1.1/proxy/network/integration/v1/sites/88f7af54-98f8-306a-a1c7-c9349722b1f6/dns/policies/a5689d61-811a-48b0-a47c-2ece038e4356" \
|
||||
-H "Accept: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"type\": \"A_RECORD\",
|
||||
\"enabled\": true,
|
||||
\"domain\": \"test.reeselink.com\",
|
||||
\"ipv4Address\": \"10.1.0.100\",
|
||||
\"ttlSeconds\": 300
|
||||
}"
|
||||
```
|
||||
103
active/device_unifi/update_dns.py
Normal file
@@ -0,0 +1,103 @@
|
||||
import json
|
||||
import os
|
||||
from typing import TypedDict
|
||||
|
||||
import requests
|
||||
|
||||
UNIFI_API_ENDPOINT = os.getenv("UNIFI_API_ENDPOINT", "https://192.168.1.1")
|
||||
UNIFI_SITE_NAME = os.getenv("UNIFI_SITE_NAME", "Default")
|
||||
UNIFI_API_KEY = os.getenv("UNIFI_API_KEY")
|
||||
|
||||
if not UNIFI_API_KEY:
|
||||
try:
|
||||
with open("active/device_unifi/secrets/api-key", "r") as f:
|
||||
UNIFI_API_KEY = f.read()
|
||||
except (FileNotFoundError, PermissionError) as e:
|
||||
print(e)
|
||||
print("UNIFI_API_KEY required.")
|
||||
|
||||
type uuid_type = str
|
||||
|
||||
|
||||
class UnifiSite(TypedDict):
|
||||
id: uuid_type
|
||||
internalReference: str
|
||||
name: str
|
||||
|
||||
|
||||
class ApiPaths:
|
||||
@classmethod
|
||||
def list_sites(cls) -> str:
|
||||
return "/proxy/network/integration/v1/sites"
|
||||
|
||||
@classmethod
|
||||
def list_records(cls, site_id: uuid_type) -> str:
|
||||
return f"/proxy/network/integration/v1/sites/{site_id}/dns/policies"
|
||||
|
||||
@classmethod
|
||||
def create_record(cls, site_id: uuid_type) -> str:
|
||||
return f"/proxy/network/integration/v1/sites/{site_id}/dns/policies"
|
||||
|
||||
@classmethod
|
||||
def update_record(cls, site_id: uuid_type, record_id: uuid_type) -> str:
|
||||
return f"/proxy/network/integration/v1/sites/{site_id}/dns/policies/{record_id}"
|
||||
|
||||
|
||||
class ApiWrapper:
|
||||
@classmethod
|
||||
def api_get(cls, path: str):
|
||||
return requests.get(
|
||||
f"{UNIFI_API_ENDPOINT}{path}",
|
||||
headers={"X-API-Key": UNIFI_API_KEY},
|
||||
verify=False,
|
||||
).json()
|
||||
|
||||
@classmethod
|
||||
def api_put(cls, path: str, body: dict):
|
||||
return requests.put(
|
||||
f"{UNIFI_API_ENDPOINT}{path}",
|
||||
headers={"X-API-Key": UNIFI_API_KEY},
|
||||
verify=False,
|
||||
json=json.dumps(body),
|
||||
).json()
|
||||
|
||||
|
||||
class ApiHelperMethods:
|
||||
@classmethod
|
||||
def site_name_to_id(cls, site_name: str) -> uuid_type:
|
||||
results = ApiWrapper.api_get(ApiPaths.list_sites())
|
||||
data: list[UnifiSite] = results.get("data")
|
||||
if not data:
|
||||
print("No sites found")
|
||||
exit(1)
|
||||
filtered_sites: list[UnifiSite] = list(
|
||||
filter(lambda data_item: data_item["name"] == site_name, data)
|
||||
)
|
||||
if not filtered_sites:
|
||||
print("Site with that name not found")
|
||||
exit(1)
|
||||
site_id = filtered_sites[0]["id"]
|
||||
return site_id
|
||||
|
||||
# @classmethod
|
||||
# def upsert_dns_ipv4_record(cls, record_name: str, ipv4_addr: str) -> uuid_type:
|
||||
|
||||
@classmethod
|
||||
def dns_record_exists(cls, record_name: str) -> bool:
|
||||
site_id = ApiHelperMethods.site_name_to_id(UNIFI_SITE_NAME)
|
||||
record_id = ApiWrapper.api_get(path=ApiPaths.list_records(site_id))
|
||||
matched_dns_records = list(
|
||||
filter(
|
||||
lambda record_item: record_item["domain"] == record_name,
|
||||
record_id["data"],
|
||||
)
|
||||
)
|
||||
if matched_dns_records:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
site_name = sys.argv[1]
|
||||
90
active/kubernetes/.$Cluster2026.drawio.bkp
Normal file
@@ -0,0 +1,90 @@
|
||||
<mxfile host="Electron" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/29.6.6 Chrome/144.0.7559.236 Electron/40.8.4 Safari/537.36" version="29.6.6">
|
||||
<diagram name="Page-1" id="sur_P5ccan6r_R6vxB1T">
|
||||
<mxGraphModel dx="1243" dy="832" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-23" parent="1" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fillColor=none;" value="Client" vertex="1">
|
||||
<mxGeometry height="440" width="200" x="840" y="120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-22" parent="1" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fillColor=none;" value="Network Admin" vertex="1">
|
||||
<mxGeometry height="440" width="200" x="560" y="120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-21" parent="1" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fillColor=none;" value="Developer" vertex="1">
|
||||
<mxGeometry height="440" width="200" y="120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-20" parent="1" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fillColor=none;" value="Cluster Admin" vertex="1">
|
||||
<mxGeometry height="440" width="200" x="280" y="120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-4" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-3" value="Create Record">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-1" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="External DNS" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="320" y="160" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-13" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-8" value="Connect">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-19" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-3" value="Get IP">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="940" y="190" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-2" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Browser" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="880" y="240" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-3" parent="1" style="ellipse;shape=cloud;whiteSpace=wrap;html=1;" value="Route53" vertex="1">
|
||||
<mxGeometry height="80" width="120" x="600" y="150" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-9" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-8" value="Allocate IP">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-7" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Metal LB" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="320" y="240" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-12" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-11" value="Forward to Gateway">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="660" y="350" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-8" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Router/Proxy" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="600" y="240" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-18" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-10" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-15" value="Create Storage">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-10" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Longhorn" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="320" y="480" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-17" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-11" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-14" value="Forward to Container">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-11" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Traefik" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="320" y="320" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-16" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-14" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-15">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-14" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Container" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="40" y="320" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-15" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Storage" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="40" y="400" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-26" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-24" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-25" value="Request Certificate">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-24" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Cert Manager" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="320" y="400" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-25" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="CA<div>(Let's Encrypt)</div>" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="600" y="400" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
96
active/kubernetes/Cluster2026.drawio
Normal file
@@ -0,0 +1,96 @@
|
||||
<mxfile host="Electron" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/29.6.6 Chrome/144.0.7559.236 Electron/40.8.4 Safari/537.36" version="29.6.6">
|
||||
<diagram name="Page-1" id="sur_P5ccan6r_R6vxB1T">
|
||||
<mxGraphModel dx="1243" dy="832" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-23" parent="1" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fillColor=none;" value="Client" vertex="1">
|
||||
<mxGeometry height="520" width="200" x="840" y="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-22" parent="1" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fillColor=none;" value="Network Admin" vertex="1">
|
||||
<mxGeometry height="520" width="200" x="560" y="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-21" parent="1" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fillColor=none;" value="Developer" vertex="1">
|
||||
<mxGeometry height="520" width="200" y="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-20" parent="1" style="rounded=0;whiteSpace=wrap;html=1;verticalAlign=top;fillColor=none;" value="Cluster Admin" vertex="1">
|
||||
<mxGeometry height="520" width="200" x="280" y="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-4" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-3" value="Create Record">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-1" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="External DNS" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="320" y="160" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-13" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-8" value="Connect">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-19" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-3" value="Get IP">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="940" y="190" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-2" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Browser" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="880" y="240" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-3" parent="1" style="ellipse;shape=cloud;whiteSpace=wrap;html=1;" value="Route53" vertex="1">
|
||||
<mxGeometry height="80" width="120" x="600" y="150" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-9" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-8" value="Allocate IP">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-7" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Metal LB" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="320" y="240" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-12" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-11" value="Forward to Gateway">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="660" y="350" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-8" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Router/Proxy" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="600" y="240" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-18" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-10" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-15" value="Create Storage">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-10" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Longhorn" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="320" y="480" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-17" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-11" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-14" value="Forward to Container">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-11" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Traefik" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="320" y="320" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-16" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-14" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-15">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-14" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Container" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="40" y="320" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-15" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Storage" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="40" y="400" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-26" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-24" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-25" value="Request Certificate">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-24" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Cert Manager" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="320" y="400" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-25" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="CA<div>(Let's Encrypt)</div>" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="600" y="400" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-29" edge="1" parent="1" source="lTIulqBT4iiTOOd0l-js-28" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="lTIulqBT4iiTOOd0l-js-14" value="Create Internal Record">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="lTIulqBT4iiTOOd0l-js-28" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="CoreDNS" vertex="1">
|
||||
<mxGeometry height="60" width="120" x="320" y="80" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
34
active/kubernetes_external-dns/demo-app.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nginx
|
||||
annotations:
|
||||
external-dns.alpha.kubernetes.io/hostname: external-dns.reeselink.com
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- port: 80
|
||||
name: http
|
||||
targetPort: 80
|
||||
selector:
|
||||
app: nginx
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- image: nginx
|
||||
name: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
@@ -1,80 +0,0 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
labels:
|
||||
app.kubernetes.io/name: external-dns
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["services","endpoints","pods","nodes"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: ["extensions","networking.k8s.io"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get","watch","list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
labels:
|
||||
app.kubernetes.io/name: external-dns
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: external-dns
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: external-dns
|
||||
namespace: kube-system # change to desired namespace: externaldns, kube-addons
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
namespace: kube-system
|
||||
labels:
|
||||
app.kubernetes.io/name: external-dns
|
||||
spec:
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: external-dns
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: external-dns
|
||||
spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.k8s.io/external-dns/external-dns:v0.14.2
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
- --domain-filter=reeseapps.com
|
||||
- --provider=aws
|
||||
- --aws-zone-type=public
|
||||
- --registry=txt
|
||||
# - --txt-owner-id=external-dns
|
||||
env:
|
||||
- name: AWS_DEFAULT_REGION
|
||||
value: us-east-1 # change to region where EKS is installed
|
||||
- name: AWS_SHARED_CREDENTIALS_FILE
|
||||
value: /.aws/externaldns-credentials
|
||||
volumeMounts:
|
||||
- name: aws-credentials
|
||||
mountPath: /.aws
|
||||
readOnly: true
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "500m"
|
||||
|
||||
volumes:
|
||||
- name: aws-credentials
|
||||
secret:
|
||||
secretName: external-dns
|
||||
@@ -1,8 +0,0 @@
|
||||
# comment out sa if it was previously created
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
namespace: kube-system
|
||||
labels:
|
||||
app.kubernetes.io/name: external-dns
|
||||
@@ -6,7 +6,7 @@ metadata:
|
||||
namespace: metallb-system
|
||||
spec:
|
||||
addresses:
|
||||
- 10.4.1.1-10.4.3.254
|
||||
- 10.4.2.32-10.4.2.47
|
||||
|
||||
---
|
||||
apiVersion: metallb.io/v1beta1
|
||||
|
||||
@@ -9,4 +9,4 @@ spec:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 32Gi
|
||||
storage: 8Gi
|
||||
|
||||
@@ -2,12 +2,17 @@ apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ .Release.Name }}
|
||||
annotations:
|
||||
metallb.universe.tf/address-pool: "default-pool"
|
||||
spec:
|
||||
externalTrafficPolicy: Cluster
|
||||
selector:
|
||||
app: {{ .Release.Name }}
|
||||
ports:
|
||||
- port: {{ .Values.port }}
|
||||
targetPort: 25565
|
||||
name: {{ .Release.Name }}
|
||||
ipFamilyPolicy: PreferDualStack
|
||||
ipFamilies:
|
||||
- IPv4
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- name: minecraft
|
||||
protocol: TCP
|
||||
port: {{ .Values.port }}
|
||||
targetPort: 25565
|
||||
selector:
|
||||
app.kubernetes.io/name: {{ .Release.Name }}
|
||||
|
||||
@@ -4,7 +4,7 @@ get_server:
|
||||
server_version: "1.21.3"
|
||||
port: 25565
|
||||
max_cpu: 4
|
||||
max_ram: 8
|
||||
max_ram: 2
|
||||
server_props: |
|
||||
enable-jmx-monitoring=false
|
||||
rcon.port=25575
|
||||
|
||||
@@ -42,7 +42,7 @@ spec:
|
||||
- name: traefik-gateway
|
||||
namespace: traefik
|
||||
hostnames:
|
||||
- "traefik-demo.reeselink.com"
|
||||
- "traefik-reese.reeselink.com"
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
|
||||
@@ -363,7 +363,8 @@ for folder in $(ls); do du --exclude .snapshots -sh $folder; done
|
||||
alias {dudir,dud}='du -h --max-depth 1 | sort -h'
|
||||
|
||||
# Calculate all file sizes in current dir
|
||||
alias {dufile,duf}='ls -lhSr'
|
||||
alias {dufile,duf}='find . -name ".snapshots" -prune -o -type f -exec du -h {} + | sort -hr'
|
||||
alias {dufiler,dufr}='find . -name ".snapshots" -prune -o -type f -exec du -h {} + | sort -h'
|
||||
```
|
||||
|
||||
### Disk Wear
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
[Network]
|
||||
IPv6=true
|
||||
Internal=true
|
||||
@@ -6,6 +6,8 @@ PublishPort=8000:8000/tcp
|
||||
PublishPort=8001:8001/tcp
|
||||
# llama.cpp instruct
|
||||
PublishPort=8002:8002/tcp
|
||||
# llama.cpp code
|
||||
PublishPort=8003:8003/tcp
|
||||
# stable-diffusion.cpp gen
|
||||
PublishPort=1234:1234/tcp
|
||||
# stable-diffusion.cpp edit
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
- [open-webui](#open-webui)
|
||||
- [lite-llm](#lite-llm)
|
||||
- [Install Services with Quadlets](#install-services-with-quadlets)
|
||||
- [API Keys](#api-keys)
|
||||
- [Internal and External Pods](#internal-and-external-pods)
|
||||
- [Llama CPP Server (Port 8000)](#llama-cpp-server-port-8000)
|
||||
- [Llama CPP Embedding Server (Port 8001)](#llama-cpp-embedding-server-port-8001)
|
||||
@@ -179,7 +180,11 @@ rsync -av --progress /home/ai/models/ /srv/models/
|
||||
|
||||
### Download models
|
||||
|
||||
In general I try to run 8 bit quantized minimum.
|
||||
In my completely subjective opinion: 5 bit quant is usually the sweet spot for
|
||||
unsloth models. Q5_K_S is usually just fine.
|
||||
|
||||
I usually download the F16 mmproj files. This is also completely subjective.
|
||||
BF16 is fine. F32 is overkill.
|
||||
|
||||
#### Text models
|
||||
|
||||
@@ -218,8 +223,13 @@ hf download --local-dir . ggml-org/Ministral-3-3B-Instruct-2512-GGUF
|
||||
##### Qwen
|
||||
|
||||
```bash
|
||||
# qwen3.6-35b-a3b
|
||||
mkdir qwen3.6-35b-a3b && cd qwen3.6-35b-a3b
|
||||
hf download --local-dir . unsloth/Qwen3.6-35B-A3B-GGUF Qwen3.6-35B-A3B-UD-Q5_K_M.gguf
|
||||
hf download --local-dir . unsloth/Qwen3.6-35B-A3B-GGUF mmproj-F16.gguf
|
||||
|
||||
# qwen3.5-27b-opus
|
||||
mkdir qwen3.5-27b-opus && qwen3.5-27b-opus
|
||||
mkdir qwen3.5-27b-opus && cd qwen3.5-27b-opus
|
||||
hf download --local-dir . Jackrong/Qwen3.5-27B-Claude-4.6-Opus-Reasoning-Distilled-GGUF Qwen3.5-27B.Q4_K_M.gguf
|
||||
hf download --local-dir . Jackrong/Qwen3.5-27B-Claude-4.6-Opus-Reasoning-Distilled-GGUF mmproj-BF16.gguf
|
||||
|
||||
@@ -555,6 +565,22 @@ podman run \
|
||||
|
||||
## Install Services with Quadlets
|
||||
|
||||
### API Keys
|
||||
|
||||
```bash
|
||||
mkdir -p /home/ai/.llama-api
|
||||
touch /home/ai/.llama-api/keys.env
|
||||
chmod 600 /home/ai/.llama-api/keys.env
|
||||
vim /home/ai/.llama-api/keys.env
|
||||
|
||||
LLAMA_API_KEY=
|
||||
|
||||
# Generate keys and append to file, then comma separate the keys
|
||||
openssl rand -base64 48 >> keys.env
|
||||
openssl rand -base64 48 >> keys.env
|
||||
openssl rand -base64 48 >> keys.env
|
||||
```
|
||||
|
||||
### Internal and External Pods
|
||||
|
||||
These will be used to restrict internet access to our llama.cpp and
|
||||
@@ -562,10 +588,10 @@ stable-diffusion.cpp services while allowing the frontend services to
|
||||
communicate with those containers.
|
||||
|
||||
```bash
|
||||
scp -r active/software_ai_stack/quadlets_pods/* deskwork-ai:.config/containers/systemd/
|
||||
scp -r active/software_ai_stack/ai-internal.* deskwork-ai:.config/containers/systemd/
|
||||
ssh deskwork-ai
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user start ai-internal-pod.service ai-external-pod.service
|
||||
systemctl --user start ai-internal-pod.service
|
||||
```
|
||||
|
||||
### Llama CPP Server (Port 8000)
|
||||
@@ -573,7 +599,7 @@ systemctl --user start ai-internal-pod.service ai-external-pod.service
|
||||
Installs the llama.cpp server to run our text models.
|
||||
|
||||
```bash
|
||||
scp -r active/software_ai_stack/quadlets_llama_think/* deskwork-ai:.config/containers/systemd/
|
||||
scp -r active/software_ai_stack/llama-think.container deskwork-ai:.config/containers/systemd/
|
||||
ssh deskwork-ai
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user restart ai-internal-pod.service
|
||||
@@ -584,7 +610,7 @@ systemctl --user restart ai-internal-pod.service
|
||||
Installs the llama.cpp server to run our embedding models
|
||||
|
||||
```bash
|
||||
scp -r active/software_ai_stack/quadlets_llama_embed/* deskwork-ai:.config/containers/systemd/
|
||||
scp -r active/software_ai_stack/llama-embed.container deskwork-ai:.config/containers/systemd/
|
||||
ssh deskwork-ai
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user restart ai-internal-pod.service
|
||||
@@ -595,7 +621,7 @@ systemctl --user restart ai-internal-pod.service
|
||||
Installs the llama.cpp server to run a constant instruct (no thinking) model for quick replies
|
||||
|
||||
```bash
|
||||
scp -r active/software_ai_stack/quadlets_llama_instruct/* deskwork-ai:.config/containers/systemd/
|
||||
scp -r active/software_ai_stack/llama-instruct.container deskwork-ai:.config/containers/systemd/
|
||||
ssh deskwork-ai
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user restart ai-internal-pod.service
|
||||
@@ -711,11 +737,11 @@ Apple M4 max
|
||||
export TOKEN=$(cat active/software_ai_stack/secrets/aipi-token)
|
||||
|
||||
# List Models
|
||||
curl https://aipi.reeseapps.com/v1/models \
|
||||
-H "Authorization: Bearer $TOKEN" | jq
|
||||
curl https://llama-instruct.reeseapps.com/v1/models \
|
||||
-H "Authorization: Bearer $TOKEN" | jq '.data'
|
||||
|
||||
# Text
|
||||
curl https://aipi.reeseapps.com/v1/chat/completions \
|
||||
curl https://llama-instruct.reeseapps.com/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
@@ -724,26 +750,21 @@ curl https://aipi.reeseapps.com/v1/chat/completions \
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "Hello, how are you?"}
|
||||
],
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 500
|
||||
}' | jq
|
||||
|
||||
# Completion
|
||||
curl https://aipi.reeseapps.com/v1/completions \
|
||||
curl https://llama-instruct.reeseapps.com/v1/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"model": "llama-instruct/instruct",
|
||||
"prompt": "Write a short poem about the ocean.",
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 500,
|
||||
"top_p": 1,
|
||||
"frequency_penalty": 0,
|
||||
"presence_penalty": 0
|
||||
"max_tokens": 500
|
||||
}' | jq
|
||||
|
||||
# Image Gen
|
||||
curl https://aipi.reeseapps.com/v1/images/generations \
|
||||
curl https://image-gen.reeselink.com/v1/images/generations \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
@@ -766,11 +787,11 @@ curl http://aipi.reeseapps.com/v1/images/edits \
|
||||
|
||||
# Embed
|
||||
curl \
|
||||
"https://aipi.reeseapps.com/v1/embeddings" \
|
||||
"https://llama-embed.reeseapps.com/v1/embeddings" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "llama-embed/embed",
|
||||
"model": "deskwork-embed/embed",
|
||||
"input":"This is the reason you ended up here:",
|
||||
"encoding_format": "float"
|
||||
}'
|
||||
@@ -789,16 +810,20 @@ podman run --rm \
|
||||
--env "HF_TOKEN=$HF_TOKEN" \
|
||||
-p 8010:8000 \
|
||||
--ipc=host \
|
||||
-e ROCBLAS_USE_HIPBLASLT=1 \
|
||||
-e TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1 \
|
||||
-e VLLM_TARGET_DEVICE=rocm \
|
||||
-e HIP_FORCE_DEV_KERNARG=1 \
|
||||
-e RAY_EXPERIMENTAL_NOSET_ROCR_VISIBLE_DEVICES=1 \
|
||||
docker.io/vllm/vllm-openai-rocm:nightly \
|
||||
--enable-offline-docs \
|
||||
|
||||
# Pick your model
|
||||
Qwen/Qwen3.5-35B-A3B --max-model-len 262144 --reasoning-parser qwen3 --enable-auto-tool-choice --tool-call-parser qwen3_coder
|
||||
Qwen/Qwen3.5-35B-A3B-FP8 --max-model-len 262144 --reasoning-parser qwen3 --enable-auto-tool-choice --tool-call-parser qwen3_coder
|
||||
Qwen/Qwen3.5-9B --max-model-len 262144 --reasoning-parser qwen3 --enable-auto-tool-choice --tool-call-parser qwen3_coder
|
||||
Qwen/Qwen3.5-35B-A3B-FP8
|
||||
google/gemma-4-26B-A4B-it
|
||||
openai/gpt-oss-120b
|
||||
|
||||
```
|
||||
|
||||
## Misc
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
- name: Create Deskwork AI Stack
|
||||
hosts: toybox-ai
|
||||
hosts: deskwork-ai
|
||||
tasks:
|
||||
- name: Create /home/ai/.config/containers/systemd
|
||||
ansible.builtin.file:
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
- ai-internal.pod
|
||||
- llama-embed.container
|
||||
- llama-instruct.container
|
||||
- llama-think.container
|
||||
- llama-code.container
|
||||
- name: Reload and start the ai-internal-pod service
|
||||
ansible.builtin.systemd_service:
|
||||
state: restarted
|
||||
22
active/software_ai_stack/install_ai_think_stack.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
- name: Create Deskwork AI Stack
|
||||
hosts: driveripper-ai
|
||||
tasks:
|
||||
- name: Create /home/ai/.config/containers/systemd
|
||||
ansible.builtin.file:
|
||||
path: /home/ai/.config/containers/systemd
|
||||
state: directory
|
||||
mode: "0755"
|
||||
- name: Copy Quadlets
|
||||
template:
|
||||
src: "{{ item }}"
|
||||
dest: "/home/ai/.config/containers/systemd/{{ item }}"
|
||||
loop:
|
||||
- ai-internal.network
|
||||
- ai-internal.pod
|
||||
- llama-think.container
|
||||
- name: Reload and start the ai-internal-pod service
|
||||
ansible.builtin.systemd_service:
|
||||
state: restarted
|
||||
name: ai-internal-pod.service
|
||||
daemon_reload: true
|
||||
scope: user
|
||||
49
active/software_ai_stack/llama-code.container
Normal file
@@ -0,0 +1,49 @@
|
||||
[Unit]
|
||||
Description=A Llama CPP Server Running a Coding Model
|
||||
|
||||
[Container]
|
||||
# Shared AI internal pod without internet access
|
||||
Pod=ai-internal.pod
|
||||
|
||||
# Image is built locally via podman build
|
||||
Image=localhost/llama-cpp-vulkan:latest
|
||||
|
||||
# Downloaded models volume
|
||||
Volume=/home/ai/models/text:/models:z
|
||||
|
||||
# GPU Device
|
||||
AddDevice=/dev/kfd
|
||||
AddDevice=/dev/dri
|
||||
|
||||
# Server command
|
||||
Exec=--port 8003 \
|
||||
-c 256000 \
|
||||
-n 65536 \
|
||||
--temp 0.7 \
|
||||
--top-p 0.8 \
|
||||
--top-k 20 \
|
||||
--repeat-penalty 1.05 \
|
||||
--perf \
|
||||
--n-gpu-layers all \
|
||||
--jinja \
|
||||
-m /models/qwen3-coder-30b-a3b/Qwen3-Coder-30B-A3B-Instruct-Q5_K_M.gguf \
|
||||
--alias code
|
||||
|
||||
# Health Check
|
||||
HealthCmd=CMD-SHELL curl --fail http://127.0.0.1:8003/health || exit 1
|
||||
HealthInterval=10s
|
||||
HealthRetries=3
|
||||
HealthStartPeriod=10s
|
||||
HealthTimeout=30s
|
||||
HealthOnFailure=kill
|
||||
|
||||
EnvironmentFile=/home/ai/.llama-api/keys.env
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
# Extend Timeout to allow time to pull the image
|
||||
TimeoutStartSec=900
|
||||
|
||||
[Install]
|
||||
# Start by default on boot
|
||||
WantedBy=multi-user.target default.target
|
||||
@@ -2,7 +2,7 @@
|
||||
Description=A Llama CPP Server For Embedding Models
|
||||
|
||||
[Container]
|
||||
# Shared AI internal pod
|
||||
# Shared AI internal pod without internet access
|
||||
Pod=ai-internal.pod
|
||||
|
||||
# Image is built locally via podman build
|
||||
@@ -18,21 +18,22 @@ AddDevice=/dev/dri
|
||||
# Server command
|
||||
Exec=--port 8001 \
|
||||
-c 0 \
|
||||
-b 1024 \
|
||||
-ub 1024 \
|
||||
--perf \
|
||||
--n-gpu-layers all \
|
||||
--models-max 1 \
|
||||
--models-dir /models \
|
||||
--embedding \
|
||||
-m /models/qwen3-embed-4b/Qwen3-Embedding-4B-Q8_0.gguf \
|
||||
-m /models/emebeddinggemma-300m/embeddinggemma-300M-BF16.gguf \
|
||||
--alias embed
|
||||
|
||||
# Health Check
|
||||
HealthCmd=CMD-SHELL curl --fail http://127.0.0.1:8001/props || exit 1
|
||||
HealthCmd=CMD-SHELL curl --fail http://127.0.0.1:8001/health || exit 1
|
||||
HealthInterval=10s
|
||||
HealthRetries=3
|
||||
HealthStartPeriod=10s
|
||||
HealthTimeout=30s
|
||||
HealthOnFailure=kill
|
||||
EnvironmentFile=/home/ai/.llama-api/keys.env
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
@@ -41,4 +42,4 @@ TimeoutStartSec=900
|
||||
|
||||
[Install]
|
||||
# Start by default on boot
|
||||
WantedBy=multi-user.target default.target
|
||||
WantedBy=multi-user.target default.target
|
||||
@@ -1,8 +1,8 @@
|
||||
[Unit]
|
||||
Description=A Llama CPP Server Running GPT OSS 120b
|
||||
Description=A Llama CPP Server Running a Non-Reasoning Model
|
||||
|
||||
[Container]
|
||||
# Shared AI internal pod
|
||||
# Shared AI internal pod without internet access
|
||||
Pod=ai-internal.pod
|
||||
|
||||
# Image is built locally via podman build
|
||||
@@ -17,29 +17,31 @@ AddDevice=/dev/dri
|
||||
|
||||
# Server command
|
||||
Exec=--port 8002 \
|
||||
-c 16000 \
|
||||
--perf \
|
||||
-v \
|
||||
--top-k 20 \
|
||||
--top-p 0.8 \
|
||||
--min-p 0 \
|
||||
--presence-penalty 1.5 \
|
||||
--repeat-penalty 1 \
|
||||
-c 262144 \
|
||||
-n 32768 \
|
||||
--temp 0.7 \
|
||||
--top-p 0.8 \
|
||||
--min-p 0.0 \
|
||||
--top-k 20 \
|
||||
--repeat-penalty 1.0 \
|
||||
--presence-penalty 1.5 \
|
||||
--reasoning-budget 0 \
|
||||
--perf \
|
||||
--n-gpu-layers all \
|
||||
--jinja \
|
||||
-m /models/qwen3.6-35b-a3b/Qwen3.6-35B-A3B-UD-Q5_K_M.gguf \
|
||||
--mmproj /models/qwen3.6-35b-a3b/mmproj-F16.gguf \
|
||||
--chat-template-kwargs '{"enable_thinking": false}' \
|
||||
-m /models/qwen3.5-35b-a3b/Qwen3.5-35B-A3B-Q8_0.gguf \
|
||||
--mmproj /models/qwen3.5-35b-a3b/mmproj-F16.gguf \
|
||||
--alias instruct
|
||||
|
||||
# Health Check
|
||||
HealthCmd=CMD-SHELL curl --fail http://127.0.0.1:8000/health || exit 1
|
||||
HealthCmd=CMD-SHELL curl --fail http://127.0.0.1:8002/health || exit 1
|
||||
HealthInterval=10s
|
||||
HealthRetries=3
|
||||
HealthStartPeriod=10s
|
||||
HealthTimeout=30s
|
||||
HealthOnFailure=kill
|
||||
EnvironmentFile=/home/ai/.llama-api/keys.env
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
@@ -48,4 +50,4 @@ TimeoutStartSec=900
|
||||
|
||||
[Install]
|
||||
# Start by default on boot
|
||||
WantedBy=multi-user.target default.target
|
||||
WantedBy=multi-user.target default.target
|
||||
@@ -1,8 +1,8 @@
|
||||
[Unit]
|
||||
Description=A Llama CPP Server Running GPT OSS 120b
|
||||
Description=A Llama CPP Server Running a Reasoning Model
|
||||
|
||||
[Container]
|
||||
# Shared AI internal pod
|
||||
# Shared AI internal pod without internet access
|
||||
Pod=ai-internal.pod
|
||||
|
||||
# Image is built locally via podman build
|
||||
@@ -17,12 +17,22 @@ AddDevice=/dev/dri
|
||||
|
||||
# Server command
|
||||
Exec=--port 8000 \
|
||||
-c 64000 \
|
||||
-c 262144 \
|
||||
-n 32768 \
|
||||
--temp 0.7 \
|
||||
--top-p 0.95 \
|
||||
--top-k 20 \
|
||||
--min-p 0.0 \
|
||||
--presence-penalty 0.0 \
|
||||
--repeat-penalty 1.0 \
|
||||
--reasoning-budget 5000 \
|
||||
-fa on \
|
||||
--perf \
|
||||
--n-gpu-layers all \
|
||||
--jinja \
|
||||
--models-max 1 \
|
||||
--models-dir /models
|
||||
-m /models/qwen3.6-35b-a3b/Qwen3.6-35B-A3B-UD-Q5_K_M.gguf \
|
||||
--mmproj /models/qwen3.6-35b-a3b/mmproj-F16.gguf \
|
||||
--alias think
|
||||
|
||||
# Health Check
|
||||
HealthCmd=CMD-SHELL curl --fail http://127.0.0.1:8000/health || exit 1
|
||||
@@ -39,4 +49,4 @@ TimeoutStartSec=900
|
||||
|
||||
[Install]
|
||||
# Start by default on boot
|
||||
WantedBy=multi-user.target default.target
|
||||
WantedBy=multi-user.target default.target
|
||||
@@ -2,7 +2,7 @@
|
||||
Description=A Stable Diffusion CPP Server for Editing Images
|
||||
|
||||
[Container]
|
||||
# Shared AI Internal pod
|
||||
# Shared AI internal pod without internet access
|
||||
Pod=ai-internal.pod
|
||||
|
||||
# Vulkan image for AMD GPU
|
||||
@@ -23,7 +23,7 @@ Exec=-l 0.0.0.0 \
|
||||
--listen-port 1235 \
|
||||
--diffusion-model /models/image/flux2-klein/flux-2-klein-9b-Q8_0.gguf \
|
||||
--vae /models/image/flux2-klein/ae.safetensors \
|
||||
--llm /models/image/flux2-klein/Qwen3-8B-Q8_0.gguf \
|
||||
--llm /models/image/flux2-klein/Qwen3-8B-Q4_K_M.gguf \
|
||||
-v \
|
||||
--sampling-method euler \
|
||||
--cfg-scale 1.0 \
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Description=A Stable Diffusion CPP Server for Generating Images
|
||||
|
||||
[Container]
|
||||
# Shared AI internal pod
|
||||
# Shared AI internal pod without internet access
|
||||
Pod=ai-internal.pod
|
||||
|
||||
# Vulkan image for AMD GPU
|
||||
@@ -23,7 +23,7 @@ Exec=-l 0.0.0.0 \
|
||||
--listen-port 1234 \
|
||||
--diffusion-model /models/image/z-turbo/z_image_turbo-Q8_0.gguf \
|
||||
--vae /models/image/z-turbo/ae.safetensors \
|
||||
--llm /models/image/z-turbo/Qwen3-4B-Instruct-2507-Q8_0.gguf \
|
||||
--llm /models/image/z-turbo/Qwen3-4B-Instruct-2507-Q4_K_M.gguf \
|
||||
-v \
|
||||
--cfg-scale 1.0 \
|
||||
--vae-conv-direct \
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# BTRFS
|
||||
|
||||
- [BTRFS](#btrfs)
|
||||
- [Disk Usage](#disk-usage)
|
||||
- [Naming Conventions](#naming-conventions)
|
||||
- [Creating an Array](#creating-an-array)
|
||||
- [Converting an Array Between RAID Versions](#converting-an-array-between-raid-versions)
|
||||
@@ -19,6 +20,15 @@ Oracle [has decent docs here](https://docs.oracle.com/en/operating-systems/oracl
|
||||
|
||||
You'll also want to [read about btrfs compression](https://thelinuxcode.com/enable-btrfs-filesystem-compression/)
|
||||
|
||||
## Disk Usage
|
||||
|
||||
With compression, the actual size on disk can be obscured. Use the following
|
||||
command to check the actual file size of all files in a directory.
|
||||
|
||||
```bash
|
||||
find . -name ".snapshots" -prune -o -type f -exec du -h {} + | sort -hr
|
||||
```
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
`poolX` is my naming convention for data pools. `pool0` is the first pool you create.
|
||||
|
||||
4
active/software_iscsi/iscsi.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# ISCSI
|
||||
|
||||
## Server
|
||||
|
||||
@@ -365,10 +365,10 @@ We'll use traefik gateway to provide ingress.
|
||||
helm repo add traefik https://traefik.github.io/charts
|
||||
helm repo update
|
||||
|
||||
# Create the traefik namespace
|
||||
kubectl create namespace traefik
|
||||
|
||||
# Generate a self‑signed certificate valid for *.reeselink.com
|
||||
mkdir active/kubernetes_traefik/secrets
|
||||
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
|
||||
-keyout active/kubernetes_traefik/secrets/tls.key -out active/kubernetes_traefik/secrets/tls.crt \
|
||||
-subj "/CN=*.reeselink.com"
|
||||
@@ -379,29 +379,33 @@ kubectl create secret tls local-selfsigned-tls \
|
||||
--namespace traefik
|
||||
|
||||
# Install the chart into the 'traefik' namespace
|
||||
helm install traefik traefik/traefik \
|
||||
helm upgrade --install traefik traefik/traefik \
|
||||
--namespace traefik \
|
||||
--values active/kubernetes_traefik/values.yaml
|
||||
|
||||
# Deploy a demo
|
||||
kubectl apply -f active/kubernetes_traefik/demo-app.yaml
|
||||
kubectl apply -f active/kubernetes_traefik/demo-route.yaml
|
||||
```
|
||||
|
||||
## Longhorn Storage
|
||||
|
||||
Longhorn provides replicated block storage via raw files on the nodes.
|
||||
|
||||
On the host you need to install iscsiadm
|
||||
|
||||
```bash
|
||||
dnf install iscsiadm
|
||||
systemctl enable --now iscsid
|
||||
```
|
||||
|
||||
```bash
|
||||
helm repo add longhorn https://charts.longhorn.io
|
||||
helm repo update
|
||||
|
||||
helm upgrade --install longhorn longhorn/longhorn \
|
||||
--namespace longhorn-system \
|
||||
--create-namespace \
|
||||
--set "httproute.enabled=true" \
|
||||
--set "httproute.parentRefs[0].name=traefik-gateway" \
|
||||
--set "httproute.parentRefs[0].namespace=traefik" \
|
||||
--set "httproute.hostnames[0]=longhorn.reeselink.com"
|
||||
--set "persistence.defaultClassReplicaCount=1"
|
||||
|
||||
# Check that the route was created
|
||||
kubectl get httproute longhorn-httproute -n longhorn-system -o jsonpath='{.status.parents[*].conditions}'
|
||||
|
||||
46
active/software_opencode/opencode.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Opencode
|
||||
|
||||
## install
|
||||
|
||||
```bash
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
```
|
||||
|
||||
## configure custom llama.cpp server
|
||||
|
||||
Opencode supports any OpenAI-compatible API. Set the following environment variables to point it at your llama.cpp server:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=""
|
||||
export OPENAI_BASE_URL="http://driveripper.reeselink.com:8000/v1"
|
||||
```
|
||||
|
||||
### persist across sessions
|
||||
|
||||
Add the exports to your shell profile (`~/.bashrc`, `~/.zshrc`, etc.):
|
||||
|
||||
```bash
|
||||
echo 'export OPENAI_API_KEY=""' >> ~/.bashrc
|
||||
echo 'export OPENAI_BASE_URL="http://driveripper.reeselink.com:8000/v1"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
### pick a model
|
||||
|
||||
After configuring the environment, launch opencode and select the model available from your llama.cpp instance:
|
||||
|
||||
```bash
|
||||
opencode
|
||||
```
|
||||
|
||||
Inside opencode, use `/model` to list available models and switch between them.
|
||||
|
||||
### verify the connection
|
||||
|
||||
Run this one-liner to confirm opencode can reach the server:
|
||||
|
||||
```bash
|
||||
OPENAI_API_KEY="" OPENAI_BASE_URL="http://driveripper.reeselink.com:8000/v1" opencode --help
|
||||
```
|
||||
|
||||
If no auth-related errors appear, the endpoint is reachable.
|
||||
@@ -86,14 +86,14 @@ dnf install openscap-scanner scap-security-guide
|
||||
|
||||
# Test with qemu
|
||||
virt-install \
|
||||
--name "fedora43-base" \
|
||||
--boot uefi,firmware.feature0.name=secure-boot,firmware.feature0.enabled=no \
|
||||
--cpu host-passthrough --vcpus sockets=1,cores=8,threads=2 \
|
||||
--ram=8192 \
|
||||
--os-variant=fedora41 \
|
||||
--os-variant=fedora43 \
|
||||
--network bridge:virbr0 \
|
||||
--graphics none \
|
||||
--console pty,target.type=virtio \
|
||||
--name "fedora43-base" \
|
||||
--import --disk "path=active/software_osbuild/secrets/fedora43base.qcow2,bus=virtio"
|
||||
```
|
||||
|
||||
|
||||
@@ -273,7 +273,7 @@ virt-install \
|
||||
--name "${VM_NAME}" \
|
||||
--boot uefi,firmware.feature0.name=secure-boot,firmware.feature0.enabled=no \
|
||||
--cpu host-passthrough --vcpus sockets=1,cores=8,threads=2 \
|
||||
--ram=8192 \
|
||||
--ram=4096 \
|
||||
--os-variant=fedora41 \
|
||||
--network bridge:virbr0 \
|
||||
--graphics none \
|
||||
|
||||
@@ -1,6 +1,83 @@
|
||||
# Wireguard
|
||||
|
||||
## Install
|
||||
## Manual Install
|
||||
|
||||
### 1. Install WireGuard
|
||||
|
||||
```bash
|
||||
sudo dnf install -y wireguard-tools qrencode
|
||||
```
|
||||
|
||||
### 2. Generate server keys
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /etc/wireguard
|
||||
cd /etc/wireguard
|
||||
sudo umask 077
|
||||
sudo wg genkey | sudo tee privatekey | sudo wg pubkey | sudo tee publickey
|
||||
```
|
||||
|
||||
### 3. Create the WireGuard config
|
||||
|
||||
```bash
|
||||
sudo tee /etc/wireguard/wg0.conf > /dev/null <<'EOF'
|
||||
[Interface]
|
||||
Address = 10.10.0.1/24
|
||||
ListenPort = 51820
|
||||
PrivateKey = INSERT_SERVER_PRIVATE_KEY_HERE
|
||||
PostUp = firewall-cmd --add-port=51820/udp
|
||||
PostDown = firewall-cmd --remove-port=51820/udp
|
||||
|
||||
[Peer]
|
||||
# Clients will be added here
|
||||
EOF
|
||||
```
|
||||
|
||||
Replace `INSERT_SERVER_PRIVATE_KEY_HERE` with the content of `/etc/wireguard/privatekey`.
|
||||
|
||||
### 4. Enable IP forwarding
|
||||
|
||||
```bash
|
||||
sudo tee /etc/sysctl.d/99-wireguard.conf > /dev/null <<'EOF'
|
||||
net.ipv4.ip_forward = 1
|
||||
net.ipv6.conf.all.forwarding = 1
|
||||
EOF
|
||||
|
||||
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf
|
||||
```
|
||||
|
||||
### 5. Start and enable WireGuard
|
||||
|
||||
```bash
|
||||
sudo systemctl enable --now wg-quick@wg0
|
||||
```
|
||||
|
||||
### 6. Configure firewalld
|
||||
|
||||
```bash
|
||||
# Allow WireGuard through the firewall
|
||||
sudo firewall-cmd --permanent --add-port=51820/udp
|
||||
|
||||
# Enable masquerading (NAT) so clients can reach the internet
|
||||
sudo firewall-cmd --permanent --add-masquerade
|
||||
|
||||
# Allow forwarded traffic from the WireGuard subnet
|
||||
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.10.0.0/24" accept'
|
||||
|
||||
# Reload and verify
|
||||
sudo firewall-cmd --reload
|
||||
sudo firewall-cmd --list-all
|
||||
```
|
||||
|
||||
### 7. Verify it's working
|
||||
|
||||
```bash
|
||||
sudo wg
|
||||
sudo wg-quick show wg0
|
||||
systemctl status wg-quick@wg0
|
||||
```
|
||||
|
||||
## Ansible Install
|
||||
|
||||
```bash
|
||||
ansible-playbook \
|
||||
@@ -37,3 +114,4 @@ read
|
||||
wg set wg0 peer $PUBKEY allowed-ips 10.10.0.$WG_IP_SUFFIX/32
|
||||
wg-quick down wg0 && wg-quick up wg0
|
||||
```
|
||||
|
||||
|
||||
178
active/vibe_agent/main.py
Normal file
@@ -0,0 +1,178 @@
|
||||
import json
|
||||
import subprocess
|
||||
from typing import Iterable
|
||||
|
||||
from openai import OpenAI
|
||||
from openai.types.chat import ChatCompletionMessageParam, ChatCompletionToolUnionParam
|
||||
|
||||
client = OpenAI(base_url="https://llama-cpp.reeselink.com", api_key="")
|
||||
|
||||
|
||||
class ToolCallController:
|
||||
def __init__(self, max_tool_calls=10):
|
||||
self.max_tool_calls = max_tool_calls
|
||||
self.tool_call_count = 0
|
||||
|
||||
def is_tool_call_allowed(self):
|
||||
return self.tool_call_count < self.max_tool_calls
|
||||
|
||||
def increment(self):
|
||||
self.tool_call_count += 1
|
||||
|
||||
def reset(self):
|
||||
self.tool_call_count = 0
|
||||
|
||||
|
||||
# Register tools
|
||||
tools: Iterable[ChatCompletionToolUnionParam] = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_servers",
|
||||
"description": "Lists the available servers to perform operations on",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"server_id": {"type": "string", "enum": ["all"]}},
|
||||
"required": ["server_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "check_updates",
|
||||
"description": "Check if a given server needs updated.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server_id": {
|
||||
"type": "string",
|
||||
}
|
||||
},
|
||||
"required": ["server_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "perform_updates",
|
||||
"description": "Update a given server to the latest package versions. Does not reboot automatically.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server_id": {
|
||||
"type": "string",
|
||||
}
|
||||
},
|
||||
"required": ["server_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "reboot",
|
||||
"description": "Reboot a given server. Waits for server to be responsive again.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server_id": {
|
||||
"type": "string",
|
||||
}
|
||||
},
|
||||
"required": ["server_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def list_servers() -> str:
|
||||
return ",".join(["ignite"])
|
||||
|
||||
|
||||
def check_updates(server_id: str):
|
||||
command_result = subprocess.run(
|
||||
["ssh", server_id, "dnf", "check-update"], capture_output=True
|
||||
)
|
||||
output = command_result.stdout.decode()
|
||||
return output
|
||||
|
||||
|
||||
def perform_updates(server_id: str):
|
||||
return f"Successfully updates {server_id}. Reboot required."
|
||||
|
||||
|
||||
def reboot(server_id: str):
|
||||
return f"Rebooted {server_id} successfully."
|
||||
|
||||
|
||||
def execute_tool(tool_name, arguments):
|
||||
if tool_name == "check_updates":
|
||||
return check_updates(**arguments)
|
||||
elif tool_name == "list_servers":
|
||||
return list_servers()
|
||||
elif tool_name == "perform_updates":
|
||||
return perform_updates(**arguments)
|
||||
elif tool_name == "reboot":
|
||||
return reboot(**arguments)
|
||||
raise ValueError(f"Unknown tool: {tool_name}")
|
||||
|
||||
|
||||
def run_conversation(user_message: str, max_tool_calls=10):
|
||||
print("Processing initial message")
|
||||
controller = ToolCallController(max_tool_calls=max_tool_calls)
|
||||
messages: Iterable[ChatCompletionMessageParam] = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a system administrator with access to a variety of administrator tools.",
|
||||
}
|
||||
]
|
||||
|
||||
messages.append({"role": "user", "content": user_message})
|
||||
|
||||
while True:
|
||||
if not controller.is_tool_call_allowed():
|
||||
messages.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": "You've reached the maximum number of tool calls. Please summarize based on available information.",
|
||||
}
|
||||
)
|
||||
break
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="qwen3.5-35b-a3b", messages=messages, tools=tools, tool_choice="auto"
|
||||
)
|
||||
|
||||
message = response.choices[0].message
|
||||
messages.append(message)
|
||||
|
||||
if message.tool_calls:
|
||||
for tool_call in message.tool_calls:
|
||||
controller.increment()
|
||||
tool_name = tool_call.function.name
|
||||
arguments = json.loads(tool_call.function.arguments)
|
||||
print(f"Attempting to call {tool_name} with arguments {arguments}...")
|
||||
result = execute_tool(tool_name, arguments)
|
||||
|
||||
messages.append(
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": str(result),
|
||||
}
|
||||
)
|
||||
else:
|
||||
break
|
||||
|
||||
return messages[-1].content
|
||||
|
||||
|
||||
# Example usage
|
||||
print(
|
||||
run_conversation(
|
||||
"Can you check if any of my servers need updates and update them? If they need rebooted, reboot them."
|
||||
)
|
||||
)
|
||||
300
active/vibe_agent/main1.py
Normal file
@@ -0,0 +1,300 @@
|
||||
import json
|
||||
import subprocess
|
||||
from typing import Iterable
|
||||
|
||||
from openai import BadRequestError, OpenAI
|
||||
from openai.types.chat import ChatCompletionMessageParam, ChatCompletionToolUnionParam
|
||||
|
||||
client = OpenAI(base_url="https://llama-cpp.reeselink.com", api_key="")
|
||||
|
||||
"""
|
||||
Tool Ideas:
|
||||
- TTS
|
||||
- Self DDOS
|
||||
- Make a funny little guy
|
||||
- Make slopbox a vtuber
|
||||
- Install arbitrary package
|
||||
- Open random ports in the firewall
|
||||
- Change system language
|
||||
- Solve a tool issue
|
||||
- Read man page
|
||||
- Read woman page
|
||||
- Read nonbinary page
|
||||
- Download a youtube video
|
||||
- ffmpeg video to gif
|
||||
- inflict damage on computer
|
||||
- Make fanart and fanfiction of our dnd characters
|
||||
- Identify potential issues in log files
|
||||
- become depressed
|
||||
- Function that switches system prompts
|
||||
- Pick patrick's optimal food choices for a day
|
||||
- Play the fish and a rice cake video (https://www.youtube.com/watch?v=uYHAR8Xzsyo)
|
||||
- Take a screenshot of the desktop, set mouse coordinates, take an action (mouse/keyboard)
|
||||
- Order radioactive material online and deliver it to botswana
|
||||
- How do we feel about Polaris Mall?
|
||||
- rm -rf Polaris Mall management's brain stem
|
||||
- Make the speakers output the "fish.mp3" at max volume
|
||||
"""
|
||||
|
||||
|
||||
class ToolCallController:
|
||||
def __init__(self, max_tool_calls=10):
|
||||
self.max_tool_calls = max_tool_calls
|
||||
self.tool_call_count = 0
|
||||
|
||||
def is_tool_call_allowed(self):
|
||||
return self.tool_call_count < self.max_tool_calls
|
||||
|
||||
def increment(self):
|
||||
self.tool_call_count += 1
|
||||
|
||||
def reset(self):
|
||||
self.tool_call_count = 0
|
||||
|
||||
|
||||
# Register tools
|
||||
tools: Iterable[ChatCompletionToolUnionParam] = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_servers",
|
||||
"description": "Lists the available servers to perform operations on",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"server_id": {"type": "string", "enum": ["all"]}},
|
||||
"required": ["server_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "check_updates",
|
||||
"description": "Check if a given server needs updated.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server_id": {
|
||||
"type": "string",
|
||||
}
|
||||
},
|
||||
"required": ["server_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "perform_updates",
|
||||
"description": "Update a given server to the latest package versions. Does not reboot automatically.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server_id": {
|
||||
"type": "string",
|
||||
}
|
||||
},
|
||||
"required": ["server_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "reboot",
|
||||
"description": "Reboot a given server. Waits for server to be responsive again.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server_id": {
|
||||
"type": "string",
|
||||
}
|
||||
},
|
||||
"required": ["server_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "install_package",
|
||||
"description": "Install a given package using `dnf` on a Fedora server.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"package_name": {
|
||||
"type": "string",
|
||||
},
|
||||
"server_id": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": ["package_name", "server_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "arbitrary_shell",
|
||||
"description": "Run any shell command in a bash shell as root.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command_string": {
|
||||
"type": "string",
|
||||
},
|
||||
"server_id": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": ["command_string", "server_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
##### FUNCTION DEFS #####
|
||||
|
||||
|
||||
def list_servers() -> str:
|
||||
return ",".join(["ignite"])
|
||||
|
||||
|
||||
def check_updates(server_id: str):
|
||||
command_result = subprocess.run(
|
||||
["ssh", server_id, "dnf", "check-update"], capture_output=True
|
||||
)
|
||||
output = command_result.stdout.decode()
|
||||
return output
|
||||
|
||||
|
||||
def perform_updates(server_id: str):
|
||||
return f"Successfully updates {server_id}. Reboot required."
|
||||
|
||||
|
||||
def reboot(server_id: str):
|
||||
return f"Rebooted {server_id} successfully."
|
||||
|
||||
|
||||
def install_package(package_name: str, server_id: str) -> str:
|
||||
command_result = subprocess.run(
|
||||
["ssh", server_id, "dnf", "install", "-y", package_name], capture_output=True
|
||||
)
|
||||
output = f"STDOUT:\n{command_result.stdout.decode()}\n\nSTDERR:\n{command_result.stderr.decode()}"
|
||||
return output
|
||||
|
||||
|
||||
def arbitrary_shell(command_string: str, server_id: str) -> str:
|
||||
try:
|
||||
command_result = subprocess.run(
|
||||
["ssh", server_id, "bash", "-c", command_string],
|
||||
capture_output=True,
|
||||
timeout=30,
|
||||
)
|
||||
output = command_result.stdout.decode()
|
||||
except subprocess.TimeoutExpired:
|
||||
output = "Command took too long and timed out."
|
||||
return output
|
||||
|
||||
|
||||
##### TOOL ROUTER #####
|
||||
|
||||
|
||||
def execute_tool(tool_name, arguments):
|
||||
if tool_name == "check_updates":
|
||||
return check_updates(**arguments)
|
||||
elif tool_name == "list_servers":
|
||||
return list_servers()
|
||||
elif tool_name == "perform_updates":
|
||||
return perform_updates(**arguments)
|
||||
elif tool_name == "reboot":
|
||||
return reboot(**arguments)
|
||||
elif tool_name == "install_package":
|
||||
return install_package(**arguments)
|
||||
elif tool_name == "arbitrary_shell":
|
||||
return arbitrary_shell(**arguments)
|
||||
raise ValueError(f"Unknown tool: {tool_name}")
|
||||
|
||||
|
||||
##### CONVERSATION #####
|
||||
|
||||
|
||||
def run_conversation(user_message: str, max_tool_calls=100):
|
||||
print("Processing initial message")
|
||||
controller = ToolCallController(max_tool_calls=max_tool_calls)
|
||||
messages: Iterable[ChatCompletionMessageParam] = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a system administrator with access to a variety of administrator tools.",
|
||||
}
|
||||
]
|
||||
|
||||
messages.append({"role": "user", "content": user_message})
|
||||
|
||||
while True:
|
||||
if not controller.is_tool_call_allowed():
|
||||
messages.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": "You've reached the maximum number of tool calls. Please summarize based on available information.",
|
||||
}
|
||||
)
|
||||
break
|
||||
|
||||
try:
|
||||
response = client.chat.completions.create(
|
||||
model="qwen3.5-35b-a3b",
|
||||
messages=messages,
|
||||
tools=tools,
|
||||
tool_choice="auto",
|
||||
)
|
||||
except BadRequestError:
|
||||
print("Request over context limit, removing last message...")
|
||||
messages.pop()
|
||||
messages.append(
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": "This tool call resulted in data that exceeded the context length limit.",
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
print()
|
||||
print(response.choices[0].message)
|
||||
|
||||
message = response.choices[0].message
|
||||
messages.append(message)
|
||||
|
||||
if message.tool_calls:
|
||||
for tool_call in message.tool_calls:
|
||||
controller.increment()
|
||||
tool_name = tool_call.function.name
|
||||
arguments = json.loads(tool_call.function.arguments)
|
||||
print(f"Attempting to call {tool_name} with arguments {arguments}...")
|
||||
result = execute_tool(tool_name, arguments)
|
||||
|
||||
messages.append(
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": str(result),
|
||||
}
|
||||
)
|
||||
else:
|
||||
break
|
||||
|
||||
try:
|
||||
return messages[-1]["content"]
|
||||
except TypeError:
|
||||
return messages[-1].content
|
||||
|
||||
|
||||
# Example usage
|
||||
print(
|
||||
run_conversation(
|
||||
"Install and set up a postgres server on all available servers. Open the firewall ports necessary. Add a default user with a simple password and tell me what the password is."
|
||||
)
|
||||
)
|
||||
81
active/vibe_mcp_server/main.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import asyncio
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import Response
|
||||
from mcp.server import Server
|
||||
from mcp.server.sse import SseServerTransport
|
||||
from starlette.responses import StreamingResponse
|
||||
|
||||
# 1. Initialize the MCP Server logic
|
||||
# This is where you define your tools, resources, and prompts
|
||||
mcp_server = Server("my-remote-server")
|
||||
|
||||
|
||||
@mcp_server.list_tools()
|
||||
async def handle_list_tools():
|
||||
"""List available tools."""
|
||||
return [
|
||||
{
|
||||
"name": "get_weather",
|
||||
"description": "Get the current weather for a location",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {"type": "string"},
|
||||
},
|
||||
"required": ["location"],
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@mcp_server.call_tool()
|
||||
async def handle_call_tool(name: str, arguments: dict):
|
||||
"""Handle tool execution."""
|
||||
if name == "get_weather":
|
||||
location = arguments.get("location", "Unknown")
|
||||
# In a real app, call an actual Weather API here
|
||||
return [
|
||||
{"type": "text", "text": f"The weather in {location} is sunny and 25°C."}
|
||||
]
|
||||
raise ValueError(f"Tool not found: {name}")
|
||||
|
||||
|
||||
# 2. Initialize FastAPI
|
||||
app = FastAPI(title="Remote MCP Server")
|
||||
|
||||
# 3. Create the SSE Transport layer
|
||||
# This object manages the connection between the web and the MCP protocol
|
||||
sse = SseServerTransport("/messages")
|
||||
|
||||
|
||||
@app.get("/sse")
|
||||
async def sse_endpoint(request: Request):
|
||||
"""
|
||||
The client connects here to start the SSE stream.
|
||||
The server will push messages to the client through this connection.
|
||||
"""
|
||||
async with sse.connect_sse(request.scope, request.receive, request._send) as (
|
||||
read_stream,
|
||||
write_stream,
|
||||
):
|
||||
# We run the MCP server using the streams provided by the SSE transport
|
||||
await mcp_server.run(
|
||||
read_stream, write_stream, mcp_server.create_initialization_options()
|
||||
)
|
||||
|
||||
|
||||
@app.post("/messages")
|
||||
async def messages_endpoint(request: Request):
|
||||
"""
|
||||
The client sends JSON-RPC messages (tool calls, etc.)
|
||||
via POST requests to this endpoint.
|
||||
"""
|
||||
await sse.handle_post_message(request.scope, request.receive, request._send)
|
||||
return Response(status_code=202)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
@@ -7,7 +7,7 @@ from email.message import EmailMessage
|
||||
from pathlib import Path
|
||||
from typing import Iterable, TypedDict, cast
|
||||
|
||||
from dotenv import dotenv_values
|
||||
from dotenv import dotenv_values, load_dotenv
|
||||
from openai import OpenAI
|
||||
from openai.types.chat import ChatCompletionMessageParam, ChatCompletionToolUnionParam
|
||||
|
||||
@@ -320,7 +320,9 @@ def run_conversation(user_message: str, max_tool_calls=10):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
client = OpenAI(base_url="https://llama-cpp.reeselink.com", api_key="")
|
||||
load_dotenv()
|
||||
api_key = os.getenv("OPENAI_API_KEY", "")
|
||||
client = OpenAI(base_url="https://llama-think.reeselink.com", api_key=api_key)
|
||||
# Example usage
|
||||
print(
|
||||
run_conversation(
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
fedora:
|
||||
hosts:
|
||||
gitea:
|
||||
immich:
|
||||
jellyfin:
|
||||
nextcloud:
|
||||
proxy:
|
||||
bricktracker:
|
||||
minecraft:
|
||||
gitea-root:
|
||||
immich-root:
|
||||
jellyfin-root:
|
||||
nextcloud-root:
|
||||
proxy-root:
|
||||
bricktracker-root:
|
||||
minecraft-root:
|
||||
borg-root:
|
||||
elk:
|
||||
elk-root:
|
||||
toybox-root:
|
||||
kube1-root:
|
||||
kube2-root:
|
||||
kube3-root:
|
||||
ai-root:
|
||||
|
||||
hardware:
|
||||
hosts:
|
||||
@@ -23,11 +24,15 @@ ai:
|
||||
hosts:
|
||||
ai-ai:
|
||||
deskwork-ai:
|
||||
toybox-ai:
|
||||
driveripper-ai:
|
||||
|
||||
caddy:
|
||||
hosts:
|
||||
proxy:
|
||||
proxy-root:
|
||||
|
||||
nginx:
|
||||
hosts:
|
||||
proxy-root:
|
||||
|
||||
wyoming:
|
||||
hosts:
|
||||
|
||||
5
automations/proxy/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Proxy Automation
|
||||
|
||||
1. Create or update the reeselink.com address in Unifi
|
||||
2. Create or update the entry in the ddns values
|
||||
3. Create or update the entry in the caddy values
|
||||
0
automations/proxy/proxy.sh
Normal file
@@ -15,12 +15,17 @@ def main():
|
||||
for _, host in enumerate(tqdm(fedora_hosts, desc="Running system updates")):
|
||||
log_file.write(f"Updating {host}\n")
|
||||
log_file.flush()
|
||||
subprocess.run(
|
||||
["ssh", host, "dnf", "upgrade", "-y"],
|
||||
stdout=log_file,
|
||||
stderr=log_file,
|
||||
check=True,
|
||||
)
|
||||
try:
|
||||
subprocess.run(
|
||||
["ssh", host, "dnf", "upgrade", "-y"],
|
||||
stdout=log_file,
|
||||
stderr=log_file,
|
||||
check=True,
|
||||
)
|
||||
except Exception as e:
|
||||
log_file.write(f"Couldn't connect to {host}. Skipping...\n")
|
||||
continue
|
||||
log_file.flush()
|
||||
log_file.write(f"Rebooting {host}\n")
|
||||
log_file.flush()
|
||||
subprocess.run(
|
||||
@@ -31,9 +36,9 @@ def main():
|
||||
)
|
||||
time.sleep(5) # wait for reboot to take effect
|
||||
booted = False
|
||||
max_attempts = 5 # seconds
|
||||
cur_attempts = 0 # seconds
|
||||
while cur_attempts > max_attempts or not booted:
|
||||
max_attempts = 10
|
||||
cur_attempts = 0
|
||||
while max_attempts > cur_attempts and not booted:
|
||||
try:
|
||||
subprocess.run(
|
||||
["ssh", host, "echo"],
|
||||
@@ -42,6 +47,8 @@ def main():
|
||||
check=True,
|
||||
timeout=2,
|
||||
)
|
||||
log_file.write(f"{host} booted!\n")
|
||||
log_file.flush()
|
||||
booted = True
|
||||
except Exception as e:
|
||||
cur_attempts += 1
|
||||
|
||||
47
presentations/kubernetes_101.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Kubernetes
|
||||
|
||||
## Network Prereqs
|
||||
|
||||
1. Allow Internal -> Load Balancer
|
||||
2. Block Load Balancer -> Internal
|
||||
3. Forward ports 22023-22122 to proxy.reeselink.com
|
||||
4. `firewall-cmd --add-port=22023-22122/tcp --permanent && firewall-cmd --reload`
|
||||
|
||||
## Creating VMs
|
||||
|
||||
```bash
|
||||
# Note: bridge1 is connected to an isolated network
|
||||
export VM_NAME=reese-k3s
|
||||
|
||||
qemu-img convert -f qcow2 -O raw \
|
||||
/srv/smb/pool0/ducoterra/images/builds/fedora43-base.qcow2 \
|
||||
/srv/vm/pool1/${VM_NAME}-boot.raw
|
||||
|
||||
virt-install \
|
||||
--boot uefi,firmware.feature0.name=secure-boot,firmware.feature0.enabled=no \
|
||||
--cpu host-passthrough --vcpus sockets=1,cores=4,threads=2 \
|
||||
--ram=4096 \
|
||||
--os-variant=fedora43 \
|
||||
--network bridge:bridge1 \
|
||||
--graphics none \
|
||||
--console pty,target.type=virtio \
|
||||
--name ${VM_NAME} \
|
||||
--import --disk "path=/srv/vm/pool1/${VM_NAME}-boot.raw,bus=virtio"
|
||||
```
|
||||
|
||||
- [ ] Add the public key to root
|
||||
|
||||
- [ ] Add the following to the proxy server's nginx.conf
|
||||
|
||||
```conf
|
||||
server {
|
||||
listen 22023;
|
||||
proxy_pass 10.4.0.159:22;
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_timeout 30s;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] `systemctl restart nginx`
|
||||
|
||||
- [ ] Send SSH command `ssh -p 22023 root@ipv4.reeselink.com`
|
||||
@@ -5,8 +5,10 @@ requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"click==8.2.1",
|
||||
"dotenv>=0.9.9",
|
||||
"fastapi>=0.135.3",
|
||||
"langchain>=1.2.13",
|
||||
"langchain-openai>=1.1.12",
|
||||
"mcp>=1.27.0",
|
||||
"mkdocs>=1.6.1",
|
||||
"openai>=2.21.0",
|
||||
"pika>=1.3.2",
|
||||
@@ -14,7 +16,14 @@ dependencies = [
|
||||
"pytest>=9.0.2",
|
||||
"pyyaml>=6.0.3",
|
||||
"requests>=2.32.5",
|
||||
"sse-starlette>=3.3.4",
|
||||
"tqdm>=4.67.3",
|
||||
"types-pyyaml>=6.0.12.20250915",
|
||||
"types-tqdm>=4.67.3.20260205",
|
||||
"uvicorn>=0.44.0",
|
||||
]
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = [
|
||||
"active/vibe_mcp",
|
||||
]
|
||||
|
||||
22
stories/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Stories
|
||||
|
||||
Stories I want to tell. Unlike the `active` project, which stores notes in no
|
||||
particular order, stories are meant to be read and enjoyed from top to bottom.
|
||||
Hopefully they teach you something.
|
||||
|
||||
## This is a mkdocs project
|
||||
|
||||
`docs/` contains all the stories.
|
||||
|
||||
`mkdocs.yml` holds the project config.
|
||||
|
||||
```bash
|
||||
# Run the mkdocs site with mkdocs serve
|
||||
uv run mkdocs serve
|
||||
```
|
||||
|
||||
## Errata
|
||||
|
||||
mkdocs has a [bug that breaks
|
||||
autoreload](https://github.com/mkdocs/mkdocs/issues/4032). This project has
|
||||
pinned click to `8.2.1` to fix it.
|
||||
92
stories/docs/10-fedora_server_admin.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# I want to build the perfect homelab server
|
||||
|
||||
Date Written: 02/11/26
|
||||
|
||||
Fedora Version: 43
|
||||
|
||||
## Intro
|
||||
|
||||
And it will run Fedora. Backstory: I ran Truenas for a long time. I started with
|
||||
Freenas, got confused by jails, switched to this new thing called Truenas,
|
||||
learned about ZFS, watched "Apps" come into existence, watched Kubernetes
|
||||
support rise and fall, watched the disaster that was Incus container/VM support
|
||||
and the subsequent "rollback" to traditional VMs, and got really tired of lack
|
||||
of control.
|
||||
|
||||
App backup and restore was never, and still isn't, well supported. Taking
|
||||
snapshots of your apps pool and then sending those snapshots to a backup drive
|
||||
un-hid the ix-systems directory (which would frequently have thousands of
|
||||
snapshots due to Truenas's liberal use of subvolumes and would slow down the UI
|
||||
immensely). App data was intentionally hidden from the user for some reason.
|
||||
Migrating between Docker, then kubernetes, then Incus was never fully planned.
|
||||
The Truenas Charts app market was awesome, but building a Truenas Chart was
|
||||
complex and required duplicating all `values.yaml` configurable parameters into
|
||||
a new yaml file that the UI could use for form-fill. They got rid of Truenas
|
||||
Charts regardless so you just have to hope that your favorite app supporter is
|
||||
comfortable rewriting their app in the new format and supporting some kind of
|
||||
migration strategy. Every six months I would expect some kind of downtime
|
||||
because Truenas would change something critical and it would inevitably impact
|
||||
my workflow.
|
||||
|
||||
So, if I'm going to be subject to the whims of a changing platform anyway (given
|
||||
Truenas is supposed to be based on Debian, aka the *stable* choice), and if I'm
|
||||
going to suffer breaking changes every 6 months no matter what I choose, then I
|
||||
may as well have the latest and greatest via a rolling kernel distro.
|
||||
|
||||
So why not Arch? Simply: SELinux. SELinux is currently not officially supported
|
||||
in Arch linux. Plus Fedora Server comes with a lot built in that I like.
|
||||
Cockpit, Firewalld, Podman, SELinux, OSBuild, and RPM support all work out of
|
||||
the box. These are, imo, the "bare bones" requirements for a server exposed to
|
||||
the internet that will run homelab services.
|
||||
|
||||
So let's get started configuring an awesome Fedora server to keep your data safe
|
||||
and run your Homelab services with minimal downtime.
|
||||
|
||||
## Installation
|
||||
|
||||
When installing Fedora from the ISO, take some time at the installation menu to
|
||||
configure some basics.
|
||||
|
||||
Don't worry about RAID for now, we can convert a single disk into a RAID 1 array
|
||||
later.
|
||||
|
||||
If you don't have an SSH key already, generate one for yourself so you can log into the server. On your local machine:
|
||||
|
||||
```bash
|
||||
# Generate the key
|
||||
# Save it to the default location (~/.ssh/id_ed25519)
|
||||
# Please please please encrypt it with a password. Something memorable. Write it down. Friends don't let friends have naked SSH keys.
|
||||
ssh-keygen -t ed25519
|
||||
```
|
||||
|
||||
1. Configure the network
|
||||
1. Set a hostname
|
||||
2. Disable ipv6 privacy extensions
|
||||
2. Configure software selection
|
||||
1. Choose anything you'd like preinstalled
|
||||
3. Create a non-root user
|
||||
1. Set a simple password for easy login, we'll change it later
|
||||
4. Configure your disk partitioning
|
||||
1. Select manual (blivet) partitioning
|
||||
2. Create a 1GB EFI system partition and mount it at `/boot/efi`
|
||||
3. Create a 1GB btrfs partition and mount it at `/boot`
|
||||
4. Create an encrypted btrfs volume with the remaining data and name it something unique, do not mount it
|
||||
5. Create a btrfs subvolume called "root" and mount it at `/`
|
||||
6. Create a btrfs subvolume called "home" and mount it at `/home`
|
||||
7. Create any other btrfs subvolumes you might need (`/var`, for example)
|
||||
5. Take note of the ipv4 and ipv6 address. Update any DNS records at this time.
|
||||
6. Install and reboot
|
||||
|
||||
## Configuration
|
||||
|
||||
Once your server boots up we'll follow a basic playbook:
|
||||
|
||||
1. Change your password
|
||||
2. Configure automatic decryption for your encrypted drives at boot with TPM2
|
||||
3. Configure the package manager and apply updates
|
||||
4. Secure SSH with Fail2Ban
|
||||
5. Install Snapper for automatic snapshots to prevent accidental file deletion
|
||||
6. Install BorgBackup for automatic backups
|
||||
7. Install VM support
|
||||
8. Build some images
|
||||
9. Run some VMs
|
||||
224
stories/docs/20-local_llms.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# I refuse to pay for LLMs
|
||||
|
||||
But I want them anyway. And I don't just want LLMs, I want:
|
||||
|
||||
1. Image Generation
|
||||
2. Image Editing
|
||||
3. Speech to Text
|
||||
4. Text to Speech
|
||||
5. Web Searching
|
||||
6. RAG Retrieval
|
||||
7. Guest accounts with time-based access
|
||||
8. Probably other things
|
||||
|
||||
On rootless podman with snapshots and backups and no compromises.
|
||||
|
||||
- [I refuse to pay for LLMs](#i-refuse-to-pay-for-llms)
|
||||
- [Create your environment](#create-your-environment)
|
||||
- [Local LLM First](#local-llm-first)
|
||||
- [Ollama](#ollama)
|
||||
- [LM Studio](#lm-studio)
|
||||
- [llama.cpp](#llamacpp)
|
||||
- [Ok, so you have a backend](#ok-so-you-have-a-backend)
|
||||
- [What about llama-server?](#what-about-llama-server)
|
||||
- [Anything LLM](#anything-llm)
|
||||
- [Open Webui](#open-webui)
|
||||
- [But we don't have image editing working](#but-we-dont-have-image-editing-working)
|
||||
- [Stable Diffusion CPP](#stable-diffusion-cpp)
|
||||
- [Making it Run with Quadlets](#making-it-run-with-quadlets)
|
||||
|
||||
## Create your environment
|
||||
|
||||
I created a user named `ai` to run all my AI services. Do that now:
|
||||
|
||||
```bash
|
||||
useradd -m ai
|
||||
loginctl enable-linger ai
|
||||
su -l ai
|
||||
mkdir -p /home/ai/.config/containers/systemd/
|
||||
mkdir -p /home/ai/.ssh
|
||||
```
|
||||
|
||||
## Local LLM First
|
||||
|
||||
On the Framework Desktop (or any AMD system) your options are ROCM or Vulkan drivers. Both are fine, with Vulkan pulling slightly ahead as of February 2026. Almost every backend you pick will support both, so pick a backend first.
|
||||
|
||||
### Ollama
|
||||
|
||||
is the natural place to start. Their "marketplace" is the best I've found for browsing models. They include short descriptions about what the models are good for and (almost) all of them work out of the box!
|
||||
|
||||
Bonus points: Ollama's API is well supported by interfaces like Anything LLM, Open Webui, a litany of F-Droid apps, and many other services.
|
||||
|
||||
Honestly, Ollama is still where I'd recommend anyone start. The installer is easy, performance is decent, the API is great, they (the Ollama team) curate models that work well on their platform, what's not to like?
|
||||
|
||||
Performance, mostly. llama.cpp just performs 20-30% better in my testing on models like gpt-oss-120b. Your mileage may vary, this is a great project.
|
||||
|
||||
### LM Studio
|
||||
|
||||
Everyone says to start with this. Ok, first of all, it's a GUI app. Yeah there's a toggle to run an API server but ain't no way I'm installing wayland on my pure, uncompromising, headless Fedora server.
|
||||
|
||||
I do have to admit it's the fastest way to get started with LLMs on desktop. But we're not here for desktops, we're here for servers. It runs llama.cpp in the backend anyway so skip past this and go for the good stuff.
|
||||
|
||||
### llama.cpp
|
||||
|
||||
We've landed on the best choice. You'll browse Hugging Face for models, be confused, and like it. You'll struggle to read the logs and feel right at home. You'll wonder why there isn't an intuitive CLI like Ollama. And you'll be rewarded with the fastest, most flexible way to run LLMs.
|
||||
|
||||
You'll need the Hugging Face CLI (`hf`). Install that.
|
||||
|
||||
First, download qwen3-vl-8b. This is a good jack of all trades model that supports vision, which is nice.
|
||||
|
||||
```bash
|
||||
# Create a directory to hold your text models
|
||||
# I put mine at /home/ai/models/text
|
||||
mkdir -p /home/ai/models/text/qwen3-vl-8b-instruct
|
||||
|
||||
# Download the model from hugging face
|
||||
hf download --local-dir /home/ai/models/text/qwen3-vl-8b-instruct Qwen/Qwen3-VL-8B-Instruct-GGUF Qwen3VL-8B-Instruct-Q4_K_M.gguf
|
||||
# Also download the "mmproj" file for this model
|
||||
# "mmproj" files allow a model to see images
|
||||
hf download --local-dir /home/ai/models/text/qwen3-vl-8b-instruct Qwen/Qwen3-VL-8B-Instruct-GGUF mmproj-Qwen3VL-8B-Instruct-Q8_0.gguf
|
||||
```
|
||||
|
||||
With our model locked and loaded, we can run the llama.cpp server. We do have to build the llama.cpp server container first though because making this any easier would be a crime.
|
||||
|
||||
```bash
|
||||
# Build the llama.cpp container image
|
||||
git clone https://github.com/ggml-org/llama.cpp.git
|
||||
cd llama.cpp
|
||||
export BUILD_TAG=$(date +"%Y-%m-%d-%H-%M-%S")
|
||||
|
||||
# Vulkan
|
||||
podman build -f .devops/vulkan.Dockerfile -t llama-cpp-vulkan:${BUILD_TAG} -t llama-cpp-vulkan:latest .
|
||||
|
||||
# Run llama server (Available on port 8000)
|
||||
# Add `--n-cpu-moe 32` to gpt-oss-120b to keep minimal number of expert in GPU
|
||||
podman run \
|
||||
--rm \
|
||||
--name llama-server-demo \
|
||||
--device=/dev/kfd \
|
||||
--device=/dev/dri \
|
||||
--pod systemd-ai-internal \
|
||||
-v /home/ai/models/text:/models:z \
|
||||
localhost/llama-cpp-vulkan:latest \
|
||||
--port 8000 \
|
||||
-c 16384 \
|
||||
--perf \
|
||||
--n-gpu-layers all \
|
||||
--jinja \
|
||||
--models-max 1 \
|
||||
--models-dir /models
|
||||
```
|
||||
|
||||
You should be able to access the llama.cpp server at http://{your-ip}:8000. From there you can select the only model you have downloaded (qwen3-vl-8b) and have a conversation.
|
||||
|
||||
## Ok, so you have a backend
|
||||
|
||||
Now we need a frontend. In my experience there are only 2 choices, but this is changing extremely fast.
|
||||
|
||||
### What about llama-server?
|
||||
|
||||
Good enough for testing. Honestly, if this meets your needs, more power to you.
|
||||
|
||||
### Anything LLM
|
||||
|
||||
I started here about a year ago. This is a fantastic frontend with RAG, speech to text, text to speech, web search, RAG, plugins, and decent user management. It supports Ollama, OpenAI, and a bunch of other backends.
|
||||
|
||||
Unfortunately, as of when I used it, there was no integrated image generation or image editing.
|
||||
|
||||
### Open Webui
|
||||
|
||||
This is, in my opinion, the best frontend experience you can get. The killer feature is side-by-side HTML rendering with your LLM response. If your LLM writes HTML/Javascript/CSS, it'll render in real time next to your chat. That's ridiculously cool.
|
||||
|
||||
It also supports image generation as a tool that your LLM can call. Prompts like "Generate an image of a dragon" will trigger a call to the image generation tool. Generated images show up in the chat and can be edited with another message.
|
||||
|
||||
```bash
|
||||
mkdir /home/ai/.env
|
||||
vim /home/ai/.env/open-webui-env
|
||||
|
||||
# Add this to the file, then save an exit
|
||||
WEBUI_SECRET_KEY="some-random-key"
|
||||
|
||||
# Will be available on port 8080
|
||||
podman run \
|
||||
-d \
|
||||
-p 8080 \
|
||||
-v open-webui:/app/backend/data \
|
||||
--env-file /home/ai/.env/open-webui-env \
|
||||
--name open-webui \
|
||||
--restart always \
|
||||
ghcr.io/open-webui/open-webui:main
|
||||
```
|
||||
|
||||
Use the following connections when configuring models/image editing:
|
||||
|
||||
| Service | Endpoint |
|
||||
| -------------------- | ----------------------------------------- |
|
||||
| llama.cpp | <http://host.containers.internal:8000> |
|
||||
| stable-diffusion.cpp | <http://host.containers.internal:1234/v1> |
|
||||
|
||||
## But we don't have image editing working
|
||||
|
||||
In the past I used stable-diffusion-webui-forge. This project relied on a very
|
||||
specific set of ROCM torch versions installed via pip from the nightly ROCM pip
|
||||
repository. I had Stable Diffusion XL and Flux1.dev working on an AMD GPU, but I
|
||||
couldn't get this working at all on the Framework Desktop.
|
||||
|
||||
I found out later this might be due to a ROCM driver bug, but we have bigger and better projects to work with.
|
||||
|
||||
### Stable Diffusion CPP
|
||||
|
||||
This project is llama.cpp equivalent for image generation. Open AI compatible API, tons of model support, excellent documentation, it's the best.
|
||||
|
||||
```bash
|
||||
# Clone and build the stable diffusion cpp container
|
||||
git clone https://github.com/leejet/stable-diffusion.cpp.git
|
||||
cd stable-diffusion.cpp
|
||||
git submodule update --init --recursive
|
||||
export BUILD_TAG=$(date +"%Y-%m-%d-%H-%M-%S")
|
||||
podman build -f Dockerfile.vulkan -t stable-diffusion-cpp:${BUILD_TAG} -t stable-diffusion-cpp:latest .
|
||||
```
|
||||
|
||||
Stable diffusion CPP supports a CLI and a web server. Let's download a model and test out the CLI.
|
||||
|
||||
```bash
|
||||
# z-turbo image model
|
||||
# Fastest image generation in 8 steps. Great a text and prompt following.
|
||||
# Lacks variety.
|
||||
mkdir -p /home/ai/models/image/z-turbo
|
||||
hf download --local-dir /home/ai/models/image/z-turbo QuantStack/FLUX.1-Kontext-dev-GGUF flux1-kontext-dev-Q4_K_M.gguf
|
||||
hf download --local-dir /home/ai/models/image/z-turbo black-forest-labs/FLUX.1-schnell ae.safetensors
|
||||
hf download --local-dir /home/ai/models/image/z-turbo unsloth/Qwen3-4B-Instruct-2507-GGUF Qwen3-4B-Instruct-2507-Q4_K_M.gguf
|
||||
|
||||
# Create our output directory
|
||||
mkdir /home/ai/output
|
||||
|
||||
# Generate an image of a photorealistic dragon.
|
||||
podman run --rm \
|
||||
-v /home/ai/models:/models:z \
|
||||
-v /home/ai/output:/output:z \
|
||||
--device /dev/kfd \
|
||||
--device /dev/dri \
|
||||
localhost/stable-diffusion-cpp:latest \
|
||||
--diffusion-model /models/image/z-turbo/z_image_turbo-Q4_K.gguf \
|
||||
--vae /models/image/z-turbo/ae.safetensors \
|
||||
--llm /models/image/z-turbo/Qwen3-4B-Instruct-2507-Q4_K_M.gguf \
|
||||
--cfg-scale 1.0 \
|
||||
-v \
|
||||
--seed -1 \
|
||||
--steps 8 \
|
||||
--vae-conv-direct \
|
||||
-H 1024 \
|
||||
-W 1024 \
|
||||
-o /output/output.png \
|
||||
-p "A photorealistic dragon"
|
||||
```
|
||||
|
||||
With any luck you should have a picture of a dragon in your output folder.
|
||||
|
||||
Since we know it works, we can tie everything together.
|
||||
|
||||
## Making it Run with Quadlets
|
||||
|
||||
Now that we have know our setup works we can glue it all together with systemd.
|
||||
|
||||
Take a look at [the framework desktop docs](https://gitea.reeseapps.com/services/homelab/src/branch/main/active/device_framework_desktop/framework_desktop.md#install-the-whole-thing-with-quadlets-tm) for the relevant commands.
|
||||
1
stories/docs/30-gpg_signing.md
Normal file
@@ -0,0 +1 @@
|
||||
# Everyone uses this GPG thing, so should I
|
||||
1
stories/docs/40-podman_rootless_hosting.md
Normal file
@@ -0,0 +1 @@
|
||||
# I want to use Podman, not Docker
|
||||
11
stories/docs/index.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Come, have a seat
|
||||
|
||||
Join me on a journey through homelab adventures. Follow along at home! These
|
||||
stores will walk you through the trials of my self hosting wins and losses.
|
||||
|
||||
The stories will be written in a way that allows you to skip past the text and
|
||||
just copy/paste the code blocks (similar to a Medium article). Each story will
|
||||
lay out its goal and the prerequisites.
|
||||
|
||||
Stories are ordered by time written, oldest to newest. They don't necessarily
|
||||
read in order, but may reference each other. No need to read each one.
|
||||
3
stories/mkdocs.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
site_name: Reese's Homelab Stories
|
||||
theme:
|
||||
name: readthedocs
|
||||
377
uv.lock
generated
@@ -2,6 +2,15 @@ version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-doc"
|
||||
version = "0.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
@@ -23,6 +32,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "26.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.1.4"
|
||||
@@ -32,6 +50,51 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.4"
|
||||
@@ -94,6 +157,59 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "46.0.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distro"
|
||||
version = "1.9.0"
|
||||
@@ -114,6 +230,22 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.135.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f7/e6/7adb4c5fa231e82c35b8f5741a9f2d055f520c29af5546fd70d3e8e1cd2e/fastapi-0.135.3.tar.gz", hash = "sha256:bd6d7caf1a2bdd8d676843cdcd2287729572a1ef524fc4d65c17ae002a1be654", size = 396524, upload-time = "2026-04-01T16:23:58.188Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/a4/5caa2de7f917a04ada20018eccf60d6cc6145b0199d55ca3711b0fc08312/fastapi-0.135.3-py3-none-any.whl", hash = "sha256:9b0f590c813acd13d0ab43dd8494138eb58e484bfac405db1f3187cfc5810d98", size = 117734, upload-time = "2026-04-01T16:23:59.328Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghp-import"
|
||||
version = "2.1.0"
|
||||
@@ -142,8 +274,10 @@ source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "dotenv" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "langchain" },
|
||||
{ name = "langchain-openai" },
|
||||
{ name = "mcp" },
|
||||
{ name = "mkdocs" },
|
||||
{ name = "openai" },
|
||||
{ name = "pika" },
|
||||
@@ -151,17 +285,21 @@ dependencies = [
|
||||
{ name = "pytest" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "requests" },
|
||||
{ name = "sse-starlette" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "types-pyyaml" },
|
||||
{ name = "types-tqdm" },
|
||||
{ name = "uvicorn" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "click", specifier = "==8.2.1" },
|
||||
{ name = "dotenv", specifier = ">=0.9.9" },
|
||||
{ name = "fastapi", specifier = ">=0.135.3" },
|
||||
{ name = "langchain", specifier = ">=1.2.13" },
|
||||
{ name = "langchain-openai", specifier = ">=1.1.12" },
|
||||
{ name = "mcp", specifier = ">=1.27.0" },
|
||||
{ name = "mkdocs", specifier = ">=1.6.1" },
|
||||
{ name = "openai", specifier = ">=2.21.0" },
|
||||
{ name = "pika", specifier = ">=1.3.2" },
|
||||
@@ -169,9 +307,11 @@ requires-dist = [
|
||||
{ name = "pytest", specifier = ">=9.0.2" },
|
||||
{ name = "pyyaml", specifier = ">=6.0.3" },
|
||||
{ name = "requests", specifier = ">=2.32.5" },
|
||||
{ name = "sse-starlette", specifier = ">=3.3.4" },
|
||||
{ name = "tqdm", specifier = ">=4.67.3" },
|
||||
{ name = "types-pyyaml", specifier = ">=6.0.12.20250915" },
|
||||
{ name = "types-tqdm", specifier = ">=4.67.3.20260205" },
|
||||
{ name = "uvicorn", specifier = ">=0.44.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -202,6 +342,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx-sse"
|
||||
version = "0.4.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
@@ -304,6 +453,33 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema"
|
||||
version = "4.26.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
{ name = "jsonschema-specifications" },
|
||||
{ name = "referencing" },
|
||||
{ name = "rpds-py" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema-specifications"
|
||||
version = "2025.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "referencing" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "langchain"
|
||||
version = "1.2.13"
|
||||
@@ -488,6 +664,31 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcp"
|
||||
version = "1.27.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "httpx" },
|
||||
{ name = "httpx-sse" },
|
||||
{ name = "jsonschema" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "pyjwt", extra = ["crypto"] },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
||||
{ name = "sse-starlette" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509, upload-time = "2026-04-02T14:48:08.88Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mergedeep"
|
||||
version = "1.3.4"
|
||||
@@ -725,6 +926,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.12.5"
|
||||
@@ -793,6 +1003,20 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.13.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
@@ -802,6 +1026,20 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyjwt"
|
||||
version = "2.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
crypto = [
|
||||
{ name = "cryptography" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.2"
|
||||
@@ -839,6 +1077,28 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.24"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/45/e23b5dc14ddb9918ae4a625379506b17b6f8fc56ca1d82db62462f59aea6/python_multipart-0.0.24.tar.gz", hash = "sha256:9574c97e1c026e00bc30340ef7c7d76739512ab4dfd428fec8c330fa6a5cc3c8", size = 37695, upload-time = "2026-04-05T20:49:13.829Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/73/89930efabd4da63cea44a3f438aeb753d600123570e6d6264e763617a9ce/python_multipart-0.0.24-py3-none-any.whl", hash = "sha256:9b110a98db707df01a53c194f0af075e736a770dc5058089650d70b4a182f950", size = 24420, upload-time = "2026-04-05T20:49:12.555Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32"
|
||||
version = "311"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
@@ -887,6 +1147,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "referencing"
|
||||
version = "0.37.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
{ name = "rpds-py" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "2026.2.28"
|
||||
@@ -986,6 +1259,72 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rpds-py"
|
||||
version = "0.30.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
@@ -1004,6 +1343,31 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sse-starlette"
|
||||
version = "3.3.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "starlette" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/26/8c/f9290339ef6d79badbc010f067cd769d6601ec11a57d78569c683fb4dd87/sse_starlette-3.3.4.tar.gz", hash = "sha256:aaf92fc067af8a5427192895ac028e947b484ac01edbc3caf00e7e7137c7bef1", size = 32427, upload-time = "2026-03-29T09:00:23.307Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl", hash = "sha256:84bb06e58939a8b38d8341f1bc9792f06c2b53f48c608dd207582b664fc8f3c1", size = 14330, upload-time = "2026-03-29T09:00:21.846Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "1.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tenacity"
|
||||
version = "9.1.4"
|
||||
@@ -1150,6 +1514,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/26/529f4beee17e5248e37e0bc17a2761d34c0fa3b1e5729c88adb2065bae6e/uuid_utils-0.14.1-cp39-abi3-win_arm64.whl", hash = "sha256:b04cb49b42afbc4ff8dbc60cf054930afc479d6f4dd7f1ec3bbe5dbfdde06b7a", size = 188132, upload-time = "2026-02-20T22:50:41.718Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.44.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "watchdog"
|
||||
version = "6.0.0"
|
||||
|
||||