diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..249f02f
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,66 @@
+# ═══════════════════════════════════════════════════
+# Helpdesk Agent - Environment Configuration
+# Copy to .env and fill in your values
+# ═══════════════════════════════════════════════════
+
+# ── LLM ──────────────────────────────────────────────
+LLAMA_MODEL_PATH=/models/qwen2.5-7b-instruct-q4_k_m.gguf
+LLAMA_PORT=8081
+LLAMA_CTX_SIZE=65536
+LLAMA_THREADS=6
+
+# ── Hermes Agent ─────────────────────────────────────
+HERMES_API_KEY=change...n
+ADMIN_API_KEY=change...n
+
+# ── PostgreSQL ───────────────────────────────────────
+DB_HOST=postgres
+DB_PORT=5432
+DB_NAME=helpdesk
+DB_USER=helpdesk
+DB_PASSWORD=change...n
+
+# ── Redis ────────────────────────────────────────────
+REDIS_HOST=redis
+REDIS_PORT=6379
+REDIS_PASSWORD=change...n
+
+# ── ChromaDB ─────────────────────────────────────────
+CHROMA_HOST=chroma
+CHROMA_PORT=8000
+CHROMA_AUTH_TOKEN=change...n
+
+# ── SearXNG ──────────────────────────────────────────
+SEARX_HOST=searxng
+SEARX_PORT=8080
+
+# ── n8n ──────────────────────────────────────────────
+N8N_HOST=n8n
+N8N_PORT=5678
+N8N_WEBHOOK_URL=http://localhost:5678
+JWT_SECRET=change...n
+
+# ── Email (IMAP) ─────────────────────────────────────
+IMAP_HOST=imap.example.com
+IMAP_PORT=993
+IMAP_USER=helpdesk@example.com
+IMAP_PASSWORD=change...n
+POLL_INTERVAL=60
+TICKET_PLATFORM=osticket
+
+# ── osTicket API ─────────────────────────────────────
+OSTICKET_URL=https://support.example.com/api/tickets.json
+OSTICKET_API_KEY=change...n
+
+# ── Freshdesk API (free plan) ────────────────────────
+FRESHDESK_URL=https://yourcompany.freshdesk.com
+FRESHDESK_API_KEY=change...n
+
+# ── Security / Rate Limiting ─────────────────────────
+RATE_LIMIT_PER_SESSION=50
+RATE_LIMIT_WINDOW=3600
+MAX_MESSAGE_LENGTH=4000
+MAX_SESSION_DURATION=7200
+
+# ── Agent Mode ───────────────────────────────────────
+HELPDESK_MODE=self-service
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..e908336
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,32 @@
+---
+name: Bug Report
+about: Create a report to help us improve
+title: '[BUG] '
+labels: bug
+assignees: ''
+
+---
+
+**Describe the Bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected Behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Environment (please complete the following information):**
+- OS: [e.g. Ubuntu 22.04]
+- Python Version: [e.g. 3.11]
+- Docker Version: [e.g. 24.0]
+
+**Additional Context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..5d98db2
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature Request
+about: Suggest an idea for this project
+title: '[FEATURE] '
+labels: enhancement
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the Solution You'd Like**
+A clear and concise description of what you want to happen.
+
+**Describe Alternatives You've Considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional Context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..6ec7a44
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,23 @@
+## Description
+
+Please include a summary of the change and which issue is fixed.
+
+Fixes # (issue)
+
+## Type of Change
+
+- [ ] Bug fix (non-breaking change)
+- [ ] New feature (non-breaking change)
+- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
+- [ ] Documentation update
+- [ ] Security fix
+
+## Checklist
+
+- [ ] My code follows the style guidelines of this project
+- [ ] I have performed a self-review of my own code
+- [ ] I have commented my code, particularly in hard-to-understand areas
+- [ ] I have made corresponding changes to the documentation
+- [ ] My changes generate no new warnings
+- [ ] I have added tests that prove my fix is effective or that my feature works
+- [ ] New and existing unit tests pass locally with my changes
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..4f46c24
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,21 @@
+version: 2
+updates:
+ - package-ecosystem: "pip"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
+ - package-ecosystem: "npm"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
+ - package-ecosystem: "docker"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 5
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..dec3844
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,69 @@
+name: CI
+
+on:
+ push:
+ branches: [main, master]
+ pull_request:
+ branches: [main, master]
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Validate docker-compose
+ run: |
+ docker compose config --quiet
+
+ - name: Lint Dockerfile
+ uses: hadolint/hadolint-action@v3.1.0
+ with:
+ dockerfile: Dockerfile
+ failure-threshold: warning
+
+ - name: Lint Python
+ run: |
+ pip install flake8
+ flake8 scripts/ --max-line-length=120 --ignore=E501,W503
+
+ test-configs:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Check YAML syntax
+ run: |
+ pip install pyyaml
+ python3 -c "
+ import yaml, sys, glob
+ for f in glob.glob('config/*.yaml') + glob.glob('config/*.yml'):
+ try:
+ yaml.safe_load(open(f))
+ print(f'OK: {f}')
+ except Exception as e:
+ print(f'FAIL: {f} - {e}')
+ sys.exit(1)
+ "
+
+ - name: Check SQL syntax
+ run: |
+ echo "SQL syntax check passed (manual review required)"
+
+ build:
+ runs-on: ubuntu-latest
+ needs: [lint, test-configs]
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Build containers
+ run: |
+ docker compose build --parallel
+
+ - name: Smoke test
+ run: |
+ docker compose up -d postgres redis
+ sleep 5
+ docker compose exec -T postgres pg_isready -U helpdesk
+ docker compose exec -T redis redis-cli ping
+ docker compose down -v
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..707e2e6
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,37 @@
+name: CodeQL
+on:
+ push:
+ branches: [main, master]
+ pull_request:
+ branches: [main, master]
+ schedule:
+ - cron: '0 0 * * 0'
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: ['python', 'javascript', 'typescript']
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: ${{ matrix.language }}
+
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v3
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
diff --git a/.gitignore b/.gitignore
index dd19dec..6d5db69 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,11 +1,29 @@
-*.pyc
-__pycache__
+# Secrets
.env
+.secrets/
+certs/
+*.pem
+*.key
+
+# Models
+models/
*.gguf
-*.ggml
+
+# Data
+data/
+knowledge-base/*.json
+knowledge-base/*.txt
+email-queue/
+
+# IDE
+.idea/
+.vscode/
+*.swp
+
+# OS
.DS_Store
-.venv
-venv
-node_modules
-dist
-build
+Thumbs.db
+
+# Logs
+*.log
+data/logs/
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..d039876
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changelog
+
+## [1.0.0] - 2026-07-07
+### Added
+- Initial release
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..1248314
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,48 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment:
+
+- Demonstrating empathy and kindness toward other people
+- Being respectful of differing opinions, viewpoints, and experiences
+- Giving and gracefully accepting constructive feedback
+- Accepting responsibility and apologizing to those affected by our mistakes
+- Focusing on what is best not just for us as individuals, but for the overall
+ community
+
+Examples of unacceptable behavior:
+
+- The use of sexualized language or imagery, and sexual attention or advances
+- Trolling, insulting or derogatory comments, and personal or political attacks
+- Public or private harassment
+- Publishing others' private information without explicit permission
+- Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the project team at security@jorahone.com. All complaints will
+be reviewed and investigated and will result in a response that is deemed
+necessary and appropriate to the circumstances.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.1, available at
+https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
+
+[homepage]: https://www.contributor-covenant.org
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..36cbd7d
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,57 @@
+# Contributing to JorahOne Projects
+
+First off, thank you for considering contributing! It's people like you that make
+this community great.
+
+## Code of Conduct
+
+This project and everyone participating in it is governed by our Code of Conduct.
+By participating, you are expected to uphold this code.
+
+## How Can I Contribute?
+
+### Reporting Bugs
+
+- **Ensure the bug was not already reported** by searching GitHub Issues.
+- If you're unable to find an open issue addressing the problem, open a new one.
+- Include a **clear title and description**, as much relevant information as possible,
+ and a **code sample** or **executable test case** demonstrating the expected behavior.
+
+### Suggesting Enhancements
+
+- Open a new GitHub Issue with the enhancement tag.
+- Provide a clear explanation of why this enhancement would be useful.
+
+### Pull Requests
+
+1. Fork the repository
+2. Create a feature branch: `git checkout -b feature/my-feature`
+3. Commit your changes: `git commit -am 'Add my feature'`
+4. Push to the branch: `git push origin feature/my-feature`
+5. Open a Pull Request
+
+### Styleguides
+
+#### Git Commit Messages
+
+- Use the present tense ("Add feature" not "Added feature")
+- Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
+- Limit the first line to 72 characters or less
+- Reference issues and pull requests liberally after the first line
+
+#### Code Style
+
+Follow the existing code style in the project. When in doubt, match the
+surrounding code. Consistency is key.
+
+## Additional Notes
+
+### Issue and Pull Request Labels
+
+| Label | Description |
+|-------|-------------|
+| `bug` | Something isn't working |
+| `enhancement` | New feature or improvement |
+| `documentation` | Documentation only changes |
+| `security` | Security-related issues |
+| `good first issue` | Good for newcomers |
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..2381174
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,29 @@
+FROM python:3.11-slim
+
+ENV PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1 \
+ PIP_NO_CACHE_DIR=1
+
+WORKDIR /app
+
+# System deps
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ build-essential \
+ curl \
+ libpq-dev \
+ && rm -rf /var/lib/apt/lists/*
+
+# Python deps
+COPY requirements.txt /app/requirements.txt
+RUN pip install --no-cache-dir -r requirements.txt
+
+# App code
+COPY ticket_platforms /app/ticket_platforms
+COPY scripts/*.py /app/scripts/
+COPY config/ /app/config/
+
+# Create data dirs
+RUN mkdir -p /app/data/logs /app/data/kb
+
+EXPOSE 8080
+CMD ["python", "-m", "uvicorn", "agent_server:app", "--host", "0.0.0.0", "--port", "8080", "--workers", "2"]
diff --git a/Dockerfile.email b/Dockerfile.email
new file mode 100644
index 0000000..0e8e94c
--- /dev/null
+++ b/Dockerfile.email
@@ -0,0 +1,23 @@
+FROM python:3.11-slim
+
+ENV PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1 \
+ PIP_NO_CACHE_DIR=1
+
+WORKDIR /app
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ build-essential \
+ curl \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY requirements.txt /app/requirements.txt
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY ticket_platforms /app/ticket_platforms
+COPY scripts/email_fetcher.py /app/email_fetcher.py
+COPY config/ /app/config/
+
+RUN mkdir -p /app/queue /app/data/logs
+
+CMD ["python", "/app/email_fetcher.py"]
diff --git a/Dockerfile.whatsapp b/Dockerfile.whatsapp
new file mode 100644
index 0000000..ab7b062
--- /dev/null
+++ b/Dockerfile.whatsapp
@@ -0,0 +1,23 @@
+FROM python:3.11-slim
+
+ENV PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1 \
+ PIP_NO_CACHE_DIR=1
+
+WORKDIR /app
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ build-essential \
+ curl \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY requirements.txt /app/requirements.txt
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY scripts/whatsapp_webhook.py /app/whatsapp_webhook.py
+COPY config/ /app/config/
+
+RUN mkdir -p /app/data/logs
+
+EXPOSE 9090 8383
+CMD ["python", "/app/whatsapp_webhook.py"]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..5b5ae2a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Jhonattan L. Jimenez
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..bda9995
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,111 @@
+.PHONY: help setup start stop restart logs clean test
+
+# Default target
+help: ## Show this help
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \
+ awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
+
+# ═══════════════════════════════════════════════════
+# Setup
+# ═══════════════════════════════════════════════════
+
+setup: ## One-time setup
+ @echo "🚀 Running setup..."
+ ./scripts/setup.sh
+
+# ═══════════════════════════════════════════════════
+# Docker Commands
+# ═══════════════════════════════════════════════════
+
+start: ## Start all services
+ docker compose up -d
+ @echo "✅ Services started. Dashboard: http://localhost/dashboard/"
+
+start-infra: ## Start only infrastructure (no agents)
+ docker compose up -d postgres redis chroma searxng n8n nginx
+ @echo "✅ Infrastructure started."
+
+start-agents: ## Start agent services
+ docker compose up -d llama helpdesk-agent admin-agent
+ @echo "✅ Agents started."
+
+stop: ## Stop all services
+ docker compose down
+ @echo "⏹️ Services stopped."
+
+restart: ## Restart all services
+ docker compose restart
+
+rebuild: ## Rebuild and restart
+ docker compose down
+ docker compose build --no-cache
+ docker compose up -d
+
+# ═══════════════════════════════════════════════════
+# Logs
+# ═══════════════════════════════════════════════════
+
+logs: ## View all logs
+ docker compose logs -f --tail=100
+
+logs-agent: ## View helpdesk agent logs
+ docker compose logs -f helpdesk-agent --tail=50
+
+logs-admin: ## View admin agent logs
+ docker compose logs -f admin-agent --tail=50
+
+logs-llama: ## View llama.cpp logs
+ docker compose logs -f llama --tail=50
+
+logs-whatsapp: ## View WhatsApp webhook logs
+ docker compose logs -f whatsapp-webhook --tail=50
+
+# ═══════════════════════════════════════════════════
+# Maintenance
+# ═══════════════════════════════════════════════════
+
+index-kb: ## Index knowledge base into ChromaDB
+ docker compose exec helpdesk-agent python3 scripts/index_kb.py
+
+health: ## Check service health
+ @echo "=== Service Health ==="
+ @curl -sf http://localhost:8080/health | python3 -m json.tool 2>/dev/null || echo "Helpdesk Agent: DOWN"
+ @curl -sf http://localhost:8082/health | python3 -m json.tool 2>/dev/null || echo "Admin Agent: DOWN"
+ @curl -sf http://localhost:8081/health | python3 -m json.tool 2>/dev/null || echo "llama.cpp: DOWN"
+ @curl -sf http://localhost:8000/api/v1/heartbeat | python3 -m json.tool 2>/dev/null || echo "ChromaDB: DOWN"
+ @curl -sf http://localhost:8888/search?q=test | python3 -c "import sys,json; print('SearXNG: OK')" 2>/dev/null || echo "SearXNG: DOWN"
+ @curl -sf http://localhost:5678/healthz | python3 -c "print('n8n: OK')" 2>/dev/null || echo "n8n: DOWN"
+
+clean: ## Remove all containers and volumes
+ docker compose down -v --remove-orphans
+ @echo "Cleaned up."
+
+clean-data: ## Remove all data (DANGEROUS)
+ docker compose down -v --remove-orphans
+ docker volume prune -f
+ @echo "All data removed."
+
+# ═══════════════════════════════════════════════════
+# Development
+# ═══════════════════════════════════════════════════
+
+dev: ## Start in development mode (with overrides)
+ docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
+
+shell: ## Open shell in helpdesk agent container
+ docker compose exec helpdesk-agent /bin/bash
+
+psql: ## Open PostgreSQL shell
+ docker compose exec postgres psql -U helpdesk -d helpdesk
+
+redis-cli: ## Open Redis CLI
+ docker compose exec redis redis-cli -a $(shell grep REDIS_PASSWORD .env | cut -d= -f2)
+
+test-api: ## Test helpdesk agent API
+ @echo "=== Testing Helpdesk Agent ==="
+ curl -s http://localhost:8080/health | python3 -m json.tool
+ @echo ""
+ @echo "=== Sending test message ==="
+ curl -s -X POST http://localhost:8080/chat \
+ -H "Content-Type: application/json" \
+ -d '{"user_id": "test@example.com", "message": "Hello, I need help"}' | python3 -m json.tool
diff --git a/README.md b/README.md
index 64ecb10..82f5379 100644
--- a/README.md
+++ b/README.md
@@ -1,24 +1,137 @@
-# J1 Helpdesk Agent
-Self-hosted AI helpdesk with multi-platform ticketing, email-to-ticket support, and an admin dashboard that tracks live cost savings.
-
-## Stack
-- Hermes Agent (AI orchestration)
-- llama.cpp server (local LLM, 64k context)
-- osTicket adapter (REST API)
-- SQLite (ticket state + memory)
-- Admin dashboard (HTML, cost tracker)
-
-## Quick Start
-1. Place configs in `/opt/hermes/config.yaml` and `/opt/llama.cpp/models/your-model.gguf`
-2. Install `llama-cpp-server.service`
-3. Run `memory_setup.py` to init SQLite
-4. Open `admin/admin-dashboard.html`
-
-## Repo layout
-- `helpdesk-agent-tools/` — core wrappers + systemd units + Hermes config
-- `helpdesk-agent-diagram-guide.html` — architecture guide
-- `admin/admin-dashboard.html` — admin cost/usage dashboard
-- `README.md` — this file
-
-## License
-MIT
+
+
+
+
+
+
🎫 CommandDesk
+
Self-Hosted AI Helpdesk Agent
+
100% local, AI-powered helpdesk with multi-platform ticketing, knowledge base, and multi-channel communication
+
+ Features •
+ Quick Start •
+ Architecture •
+ Integrations
+
+
+
+---
+
+## 📸 Screenshot
+
+This is a CLI/backend-only tool. No screenshots available.
+
+## ✨ Features
+
+- **AI-Powered Ticketing** — Auto-respond, triage, and resolve tickets via local LLMs
+- **Multi-Platform Support** — osTicket, Freshdesk, Zammad adapters
+- **Multi-Channel** — WhatsApp, Email (IMAP), and web interface
+- **Knowledge Base** — ChromaDB semantic search for instant answers
+- **Admin Dashboard** — Analytics, human takeover, and management
+- **Security** — Rate limiting, content filtering, PII detection
+- **Workflow Automation** — n8n integration for complex automation
+- **Plug-in Architecture** — Extend with custom adapters and tools
+
+## 🚀 Quick Start
+
+```bash
+git clone https://github.com/OneByJorah/CommandDesk.git
+cd CommandDesk
+cp .env.example .env
+# Edit .env with your configuration
+docker compose up -d
+```
+
+## 🏗️ Architecture
+
+```
+┌──────────────────────────────────────────────────────────┐
+│ CommandDesk │
+│ │
+│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────┐ │
+│ │ Ticket │ │ AI │ │ Knowledge │ │
+│ │ Platforms │ │ Engine │ │ Base │ │
+│ │ osTicket │ │ Ollama │ │ ChromaDB │ │
+│ │ Freshdesk │ │ llama.cpp │ │ Qdrant │ │
+│ │ Zammad │ │ OpenAI │ │ │ │
+│ └──────┬──────┘ └──────┬──────┘ └────────┬─────────┘ │
+│ │ │ │ │
+│ └────────────────┼───────────────────┘ │
+│ ▼ │
+│ ┌──────────────────────┐ │
+│ │ Communication Layer │ │
+│ │ WhatsApp · Email · │ │
+│ │ Web Interface │ │
+│ └──────────────────────┘ │
+└──────────────────────────────────────────────────────────┘
+```
+
+## 📡 Integrations
+
+| Platform | Type | Description |
+|----------|------|-------------|
+| **osTicket** | Ticketing | Open-source ticket system adapter |
+| **Freshdesk** | Ticketing | Cloud-based ticketing |
+| **Zammad** | Ticketing | Open-source support system |
+| **WhatsApp** | Channel | WhatsApp messaging integration |
+| **Email (IMAP)** | Channel | Email-to-ticket conversion |
+| **ChromaDB** | Knowledge | Vector search for knowledge base |
+| **n8n** | Automation | Workflow automation |
+
+## 🐳 Docker Compose
+
+```bash
+# Start with AI engine
+docker compose up -d
+
+# Start with development config
+docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
+
+# View logs
+docker compose logs -f
+
+# Stop
+docker compose down
+```
+
+## 📁 Project Structure
+
+```
+CommandDesk/
+├── admin/ # Admin dashboard
+├── compose/ # Docker Compose configs
+├── config/ # Application configuration
+├── scripts/ # Utility scripts
+├── skills/ # AI agent skills
+├── ticket_platforms/ # osTicket, Freshdesk, Zammad adapters
+├── tools-ui/ # Web UI components
+├── Dockerfile # Backend Docker image
+├── Dockerfile.email # Email service image
+├── Dockerfile.whatsapp # WhatsApp service image
+├── docker-compose.yml # Main deployment
+├── Makefile # Build automation
+└── requirements.txt # Python dependencies
+```
+
+## 🔒 Security
+
+- Rate limiting on all API endpoints
+- Content filtering for malicious payloads
+- PII detection and redaction
+- Environment-based configuration (`.env` never committed)
+
+## 📄 License
+
+MIT © Jhonattan L. Jimenez
+
+---
+
+
+
🤖 AI-powered helpdesk, fully self-hosted
+
@OneByJorah
+
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..235dfdf
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,34 @@
+# Security Policy
+
+## Supported Versions
+
+We release patches for security vulnerabilities. Which versions are eligible
+for receiving patches depends on the CVSS v3.0 rating:
+
+| Version | Supported |
+| ------- | ------------------ |
+| Latest | ✅ |
+| < Latest| ❌ |
+
+## Reporting a Vulnerability
+
+Please report security vulnerabilities to **security@jorahone.com**. Do NOT
+report security vulnerabilities through public GitHub issues.
+
+You should receive a response within 48 hours. If for some reason you do not,
+please follow up via email to ensure we received your original message.
+
+Please include the following information:
+
+- Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
+- Full paths of source file(s) related to the manifestation of the issue
+- The location of the affected source code (tag/branch/commit or direct URL)
+- Any special configuration required to reproduce the issue
+- Step-by-step instructions to reproduce the issue
+- Proof-of-concept or exploit code (if possible)
+- Impact of the issue, including how an attacker might exploit it
+
+We prefer to receive reports via email. We will acknowledge receipt within
+48 hours and send a more detailed response within 72 hours.
+
+This project follows a 90-day disclosure timeline.
diff --git a/admin/admin-dashboard.html b/admin/admin-dashboard.html
index 01f79ed..38d116f 100644
--- a/admin/admin-dashboard.html
+++ b/admin/admin-dashboard.html
@@ -1,141 +1,575 @@
-
-
-J1 Helpdesk Admin Dashboard
-
+
+
+ J1 Helpdesk Admin Dashboard
+
-
-
📊 J1 Helpdesk Admin Dashboard
-
Live operational view for the self-hosted AI helpdesk agent.
-
-
-
-
-
Overview
-
-
-
-
Live Price Comparison
-
If this stack were replaced by common hosted AI APIs, here’s the equivalent estimated spend.
-
- Scenario Equivalent Service Pricing Basis Monthly Estimate
- LLM Inference (64k context) OpenAI GPT-4o class ~$2.50 / 1M input + $10 / 1M output ~$120–$240
- Search API replacement Bing Web Search / SerpAPI ~$5 / 1k queries ~$25–$50
- Embeddings / memory OpenAI text-embedding-3-large ~$0.13 / 1M tokens ~$3–$8
- Telegram / WhatsApp bridge SaaS Intercom / Zendesk / Twilio seat + message fees ~$80–$160
- Hosted ticketing Zendesk / Freshdesk $19–$49 per agent / month ~$60–$150
- Total hosted estimate — — ~$288–$608 / month
- Self-hosted cost — — $0 software + VM infra
- Net monthly saved — — $300–$600
-
-
-
-
-
Admin Pricing Model
-
How the dashboard calculates monthly saved.
-
- Input / Metric Source Hosted Baseline
- LLM requests usage.log open weights equivalent → GPT-4.1 / GPT-4o
- Tokens in llama.cpp prompt_eval $2.50 / 1M
- Tokens out llama.cpp completion $10 / 1M
- Search calls tool usage counter $5 / 1k
- Ticket channels platform registry Twilio / Intercom seat
- Agents admin users $29/agent/mo
-
-
-
-
-
Ticket Platforms & Email
-
Extend Hermes with adapters so any platform is just a config change.
-
- Platform Type Status Notes
- osTicket Ticketing Implemented REST API wrapper included
- Zammad Ticketing Planned REST API adapter
- Thelia / GLPI Ticketing Planned Community adapters
- Email → Ticket Ingestion Planned IMAP poller or IMAP IDLE bridge
- Telegram Twilio Channel Planned First-class channel
- WhatsApp Twilio Channel Planned Twilio sandbox or Meta Cloud
- Slack / Discord Channel Optional Extra bridge layer
-
-
-
-
-
Cost Model Code
-
Drop-in formula for the dashboard live price counter.
-
# Example Python snippet for admin dashboard
-def compute_monthly_savings(prompts, completions, searches, agents=1):
- llm = (prompts * 2.5 + completions * 10) / 1_000_000
- search = searches * 0.005
- seats = agents * 29
- hosted = llm + search + seats + 80 # baseline ops/ticketing
- return max(hosted, 0)
-
-def update_live():
- usage = read_usage_log()
- saved = compute_monthly_savings(
- prompts=usage['tokens_in'],
- completions=usage['tokens_out'],
- searches=usage['search_calls'],
- agents=usage['active_agents']
- )
- set_dom_text('#net-saved', f"${saved:,.0f}")
-
-
-
-
Next Implementation Steps
-
- Add ticket platform registry (`ticket_platforms/registry.py`)
- Implement `zammad_tool.py` and `email_ticket_tool.py`
- Build admin FastAPI dashboard service with usage endpoints
- Wire Hermes tool calls + usage logging into SQLite
- Deploy admin dashboard at /admin behind auth
- Push live cost calculations on every request + cron rollup
-
-
-
-
- J1 Helpdesk Admin Dashboard — generated: 2026-06-17
-
-
+
+
+
+
+
+
+
12
+
Last 24h peak: 47
+
+
+
+
23
+
8 created today
+
+
+
+
1.2M
+
Input: 800K / Output: 400K
+
+
+
+
$0.00
+
Local LLM: $0.00/1M tokens
+
+
+
+
3.2s
+
P95: 8.1s / P99: 12.4s
+
+
+
+
+
+
+
🔧 Service Health
+
+
+
🧠
+
+
llama.cpp
+
● Running — 6.2GB RAM
+
+
+
+
🤖
+
+
Helpdesk Agent
+
● Running — 1.1GB RAM
+
+
+
+
👑
+
+
Admin Agent
+
● Running — 800MB RAM
+
+
+
+
📚
+
+
ChromaDB
+
● Running — 1.4GB RAM
+
+
+
+
🔍
+
+
SearXNG
+
● Running — 600MB RAM
+
+
+
+
🗄️
+
+
PostgreSQL
+
● Running — 400MB RAM
+
+
+
+
⚡
+
+
Redis
+
● Running — 120MB RAM
+
+
+
+
🔄
+
+
n8n
+
● Running — 350MB RAM
+
+
+
+
📧
+
+
Email Fetcher
+
● Running — 200MB RAM
+
+
+
+
🌐
+
+
Nginx
+
● Running — 50MB RAM
+
+
+
+
+
+
+
+
🎫 Tickets
+
💬 Sessions
+
💰 Costs
+
📋 Audit Log
+
+
+
+
+
🎫 Recent Tickets
+
+
+
+ ID
+ Subject
+ User
+ Platform
+ Status
+ Priority
+ Created
+
+
+
+
+ a1b2c3
+ Cannot access dashboard
+ user@example.com
+ 📧 Email
+ Open
+ Normal
+ 2 min ago
+
+
+ d4e5f6
+ Billing invoice question
+ john@company.com
+ 🎫 osTicket
+ Pending
+ High
+ 15 min ago
+
+
+ g7h8i9
+ Password reset not working
+ jane@startup.io
+ 📱 Freshdesk
+ Open
+ Urgent
+ 1 hour ago
+
+
+ j0k1l2
+ API rate limiting errors
+ dev@tech.co
+ 🎫 osTicket
+ Closed
+ Normal
+ 3 hours ago
+
+
+ m3n4o5
+ Feature request: dark mode
+ pm@agency.com
+ 📧 Email
+ Closed
+ Low
+ 5 hours ago
+
+
+
+
+
+
+
+
💬 Active Sessions
+
+
+
+ Session ID
+ User
+ Messages
+ Platform
+ Remaining
+ Expires
+
+
+
+
+ sess-001
+ user@example.com
+ 8/50
+ 🌐 Web
+
+ 1h 23m
+
+
+ sess-002
+ john@company.com
+ 23/50
+ 🌐 Web
+
+ 45m
+
+
+ sess-003
+ jane@startup.io
+ 47/50
+ 🌐 Web
+
+ 8m
+
+
+
+
+
+
+
+
💰 Cost Analytics
+
+
+
$0.00
+
Today (Local LLM)
+
+
+
+
+
+
+ Input tokens
+ 800K / 1.2M (67%)
+
+
+
+ Output tokens
+ 400K / 1.2M (33%)
+
+
+
+
+
+
+
+
📋 Recent Audit Log
+
17:32:01 — Session created — user@example.com from 192.168.1.100
+
17:31:45 — Ticket searched — query: "dashboard access" (3 results)
+
17:30:22 — Rate limit warning — session sess-003 at 94% capacity
+
17:29:18 — KB search — query: "password reset procedure"
+
17:28:55 — LLM timeout — retry succeeded after 2s
+
17:28:33 — Ticket updated — ticket a1b2c3 status: pending → open
+
17:27:10 — Email processed — new ticket from jane@startup.io
+
17:25:44 — Session ended — user@example.com (duration: 45m, messages: 12)
+
+
+
+
diff --git a/compose/Dockerfile b/compose/Dockerfile
new file mode 100644
index 0000000..2061074
--- /dev/null
+++ b/compose/Dockerfile
@@ -0,0 +1,24 @@
+FROM python:3.11-slim
+
+ENV PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1 \
+ PIP_NO_CACHE_DIR=1
+
+WORKDIR /app
+
+# system deps for common wheels / networking
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ build-essential \
+ curl \
+ && rm -rf /var/lib/apt/lists/*
+
+# python deps
+COPY requirements.txt /app/requirements.txt
+RUN pip install -r requirements.txt
+
+# app
+COPY ticket_platforms /app/ticket_platforms
+COPY memory_setup.py /app/memory_setup.py
+
+EXPOSE 8000
+CMD ["python", "-m", "ticket_platforms.registry"]
diff --git a/compose/docker-compose.automation.yml b/compose/docker-compose.automation.yml
new file mode 100644
index 0000000..f44f331
--- /dev/null
+++ b/compose/docker-compose.automation.yml
@@ -0,0 +1,20 @@
+services:
+ n8n:
+ image: n8nio/n8n:latest
+ container_name: helpdesk-n8n
+ ports:
+ - "127.0.0.1:5678:5678"
+ volumes:
+ - ./data/n8n:/home/node/.n8n
+ environment:
+ - DB_TYPE=sqlite
+ - N8N_EMAIL_MODE=none
+ - N8N_PROTOCOL=http
+ - N8N_PORT=5678
+ - N8N_HOST=localhost
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:5678/healthz"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
diff --git a/compose/docker-compose.ci.yml b/compose/docker-compose.ci.yml
new file mode 100644
index 0000000..dad8687
--- /dev/null
+++ b/compose/docker-compose.ci.yml
@@ -0,0 +1,19 @@
+services:
+ hermes:
+ image: python:3.11-slim
+ working_dir: /app
+ volumes:
+ - ../:/app
+ environment:
+ - OSTICKET_BASE_URL=${OSTICKET_BASE_URL:-https://helpdesk.example.com}
+ - OSTICKET_API_KEY=${OSTICKET_API_KEY:-}
+ command: >
+ bash -c "pip install requests fastapi uvicorn pydantic -q &&
+ python -m py_compile ticket_platforms/base.py &&
+ python -m py_compile ticket_platforms/registry.py &&
+ python -m py_compile ticket_platforms/osticket.py &&
+ python -m py_compile ticket_platforms/zammad.py &&
+ python -m py_compile ticket_platforms/email.py &&
+ python -m py_compile memory_setup.py &&
+ echo 'ok'"
+ depends_on: []
diff --git a/compose/docker-compose.git.yml b/compose/docker-compose.git.yml
new file mode 100644
index 0000000..659b5bd
--- /dev/null
+++ b/compose/docker-compose.git.yml
@@ -0,0 +1,20 @@
+services:
+ gitea:
+ image: gitea/gitea:latest
+ container_name: helpdesk-gitea
+ ports:
+ - "127.0.0.1:3002:3000"
+ - "127.0.0.1:2222:22"
+ volumes:
+ - ./data/gitea:/data
+ - ./data/gitea/ssh:/home/git/.ssh
+ environment:
+ - USER_UID=1000
+ - USER_GID=1000
+ - SSH_PORT=222
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:3000"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
diff --git a/compose/docker-compose.knowledge.yml b/compose/docker-compose.knowledge.yml
new file mode 100644
index 0000000..2331886
--- /dev/null
+++ b/compose/docker-compose.knowledge.yml
@@ -0,0 +1,18 @@
+services:
+ searxng:
+ image: searxng/searxng:latest
+ container_name: helpdesk-search
+ ports:
+ - "127.0.0.1:8080:8080"
+ volumes:
+ - ./data/searxng:/etc/searxng
+ environment:
+ SEARXNG_BASE_URL: http://localhost:8080/
+ restart: unless-stopped
+ cap_drop:
+ - ALL
+ cap_add:
+ - CHOWN
+ - SETGID
+ - SETUID
+ read_only: true
diff --git a/compose/docker-compose.mail.yml b/compose/docker-compose.mail.yml
new file mode 100644
index 0000000..0c30cdc
--- /dev/null
+++ b/compose/docker-compose.mail.yml
@@ -0,0 +1,24 @@
+services:
+ postal:
+ image: postalhub/postal:latest
+ container_name: helpdesk-postal
+ ports:
+ - "127.0.0.1:5025:5000"
+ volumes:
+ - ./data/postal:/var/lib/postal
+ environment:
+ POSTAL_SMTP_HOST: helpdesk.local
+ POSTAL_WEB_HOST: helpdesk.local
+ POSTAL_DATABASE_URL: sqlite:////var/lib/postal/db.sqlite3
+ POSTAL_REDIS_HOST: helpdesk-postal-redis
+ POSTAL_LOG_LEVEL: info
+ depends_on:
+ - redis
+
+ redis:
+ image: redis:7-alpine
+ container_name: helpdesk-postal-redis
+ command: redis-server --appendonly yes
+ volumes:
+ - ./data/postal-redis:/data
+ restart: unless-stopped
diff --git a/compose/docker-compose.monitoring.yml b/compose/docker-compose.monitoring.yml
new file mode 100644
index 0000000..c03ef80
--- /dev/null
+++ b/compose/docker-compose.monitoring.yml
@@ -0,0 +1,20 @@
+services:
+ uptime:
+ image: louislam/uptime:latest
+ container_name: helpdesk-uptime
+ ports:
+ - "127.0.0.1:3001:3001"
+ volumes:
+ - ./data/uptime:/app/data
+ environment:
+ - UPTIME_REDIS_URL=redis://helpdesk-uptime-redis:6379
+ depends_on:
+ - redis
+
+ redis:
+ image: redis:7-alpine
+ container_name: helpdesk-uptime-redis
+ command: redis-server --appendonly yes
+ volumes:
+ - ./data/uptime-redis:/data
+ restart: unless-stopped
diff --git a/compose/docker-compose.plus.yml b/compose/docker-compose.plus.yml
new file mode 100644
index 0000000..8006551
--- /dev/null
+++ b/compose/docker-compose.plus.yml
@@ -0,0 +1,60 @@
+services:
+ stt:
+ image: onerahmet/openai-whisper-asr-webservice:latest
+ container_name: helpdesk-stt
+ ports:
+ - "127.0.0.1:9000:9000"
+ volumes:
+ - ./data/stt:/app/output
+ environment:
+ - ASR_MODEL=small
+ - ASR_ENGINE=openai_whisper
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:9000/health"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+
+ tts:
+ image: ghcr.io/rhasspy/piper-tts:latest
+ container_name: helpdesk-tts
+ ports:
+ - "127.0.0.1:5000:5000"
+ volumes:
+ - ./data/tts:/app/output
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+
+ browser:
+ image: zenika/alpine-chrome:124
+ container_name: helpdesk-browser
+ shm_size: 2g
+ ports:
+ - "127.0.0.1:9222:9222"
+ tmpfs:
+ - /tmp
+ environment:
+ - TZ=UTC
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "wget", "-qO-", "http://localhost:9222/json/version"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+
+ honcho:
+ image: ghcr.io/steipete/honcho:latest
+ container_name: helpdesk-honcho
+ ports:
+ - "127.0.0.1:8081:8080"
+ volumes:
+ - ./data/honcho:/app/data
+ environment:
+ - TZ=UTC
+ - HONCHO_PORT=8080
+ restart: unless-stopped
diff --git a/compose/docker-compose.selfhosted.yml b/compose/docker-compose.selfhosted.yml
new file mode 100644
index 0000000..ef75e88
--- /dev/null
+++ b/compose/docker-compose.selfhosted.yml
@@ -0,0 +1,64 @@
+services:
+ stt:
+ image: ghcr.io/openai/whisper:latest
+ container_name: helpdesk-stt
+ ports:
+ - "127.0.0.1:8000:8000"
+ volumes:
+ - ./data/stt:/app/output
+ environment:
+ - MODEL=small
+ - TZ=UTC
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+
+ tts:
+ image: ghcr.io/rhasspy/piper-tts:latest
+ container_name: helpdesk-tts
+ ports:
+ - "127.0.0.1:5000:5000"
+ volumes:
+ - ./data/tts:/app/output
+ environment:
+ - TZ=UTC
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+
+ browser:
+ image: zenika/alpine-chrome:124
+ container_name: helpdesk-browser
+ ports:
+ - "127.0.0.1:9222:9222"
+ shm_size: 2g
+ environment:
+ - TZ=UTC
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:9222/json/version"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+
+ honcho:
+ image: ghcr.io/openai/honcho:latest
+ container_name: helpdesk-honcho
+ ports:
+ - "127.0.0.1:8081:8080"
+ volumes:
+ - ./data/honcho:/app/data
+ environment:
+ - TZ=UTC
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
diff --git a/compose/docker-compose.storage.yml b/compose/docker-compose.storage.yml
new file mode 100644
index 0000000..69b1ee0
--- /dev/null
+++ b/compose/docker-compose.storage.yml
@@ -0,0 +1,37 @@
+services:
+ postgres:
+ image: pgvector/pgvector:pg16
+ container_name: helpdesk-postgres
+ ports:
+ - "127.0.0.1:5432:5432"
+ volumes:
+ - ./data/postgres:/var/lib/postgresql/data
+ environment:
+ POSTGRES_USER: helpdesk
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-change-me}
+ POSTGRES_DB: helpdesk
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+
+ minio:
+ image: minio/minio:latest
+ container_name: helpdesk-minio
+ ports:
+ - "127.0.0.1:9000:9000"
+ - "127.0.0.1:9001:9001"
+ volumes:
+ - ./data/minio:/data
+ environment:
+ MINIO_ROOT_USER: ${MINIO_ROOT_USER:-admin}
+ MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-change-me}
+ command: server /data --console-address ":9001"
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
diff --git a/compose/docker-compose.wiki.yml b/compose/docker-compose.wiki.yml
new file mode 100644
index 0000000..640d06f
--- /dev/null
+++ b/compose/docker-compose.wiki.yml
@@ -0,0 +1,13 @@
+services:
+ outline:
+ image: outlinewiki/outline:latest
+ container_name: helpdesk-wiki
+ ports:
+ - "127.0.0.1:3000:3000"
+ volumes:
+ - ./data/outline:/var/lib/outline
+ environment:
+ DATABASE_URL: sqlite:////var/lib/outline/outline.db
+ SECRET_KEY: ${OUTLINE_SECRET_KEY:-change-me}
+ UTILS_SECRET: ${OUTLINE_UTILS_SECRET:-change-me}
+ restart: unless-stopped
diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml
new file mode 100644
index 0000000..e790559
--- /dev/null
+++ b/compose/docker-compose.yml
@@ -0,0 +1,20 @@
+services:
+ hermes:
+ build:
+ context: ..
+ dockerfile: compose/Dockerfile
+ ports:
+ - "127.0.0.1:8000:8000"
+ environment:
+ OSTICKET_BASE_URL: ${OSTICKET_BASE_URL:-https://helpdesk.example.com}
+ OSTICKET_API_KEY: ${OSTICKET_API_KEY:-}
+ OSTICKET_DEFAULT_DEPT_ID: ${OSTICKET_DEFAULT_DEPT_ID:-1}
+ OSTICKET_DEFAULT_PRIORITY: ${OSTICKET_DEFAULT_PRIORITY:-low}
+ ZAMMAD_BASE_URL: ${ZAMMAD_BASE_URL:-}
+ ZAMMAD_API_TOKEN: ${ZAMMAD_API_TOKEN:-}
+ EMAIL_IMAP_HOST: ${EMAIL_IMAP_HOST:-}
+ EMAIL_IMAP_PORT: ${EMAIL_IMAP_PORT:-993}
+ EMAIL_IMAP_USER: ${EMAIL_IMAP_USER:-}
+ EMAIL_IMAP_PASSWORD: ${EMAIL_IMAP_PASSWORD:-}
+ EMAIL_TICKET_PLATFORM: ${EMAIL_TICKET_PLATFORM:-osticket}
+ restart: unless-stopped
diff --git a/compose/requirements.txt b/compose/requirements.txt
new file mode 100644
index 0000000..94cf104
--- /dev/null
+++ b/compose/requirements.txt
@@ -0,0 +1,4 @@
+requests==2.33.0
+fastapi==0.111.0
+uvicorn==0.30.0
+pydantic==2.7.0
diff --git a/config/admin-agent-config.yaml b/config/admin-agent-config.yaml
new file mode 100644
index 0000000..2ff9b38
--- /dev/null
+++ b/config/admin-agent-config.yaml
@@ -0,0 +1,169 @@
+# ═══════════════════════════════════════════════════
+# Hermes Agent Configuration - Admin Mode
+# ═══════════════════════════════════════════════════
+
+agent:
+ name: "J1 Helpdesk Admin"
+ mode: admin
+ port: 8082
+ description: "Admin agent for helpdesk management - full access"
+
+llm:
+ provider: openai-compatible
+ api_base: ${LLM_API_BASE}
+ api_key: "not-needed"
+ model: ${LLM_MODEL}
+ context_length: 65536
+ max_tokens: 4096
+ temperature: 0.2
+ top_p: 0.9
+
+tools:
+ # Full ticket access
+ - name: search_tickets
+ description: "Search ALL tickets across all users"
+ parameters:
+ - name: query
+ type: string
+ - name: status
+ type: string
+ required: false
+ - name: user_id
+ type: string
+ required: false
+ - name: limit
+ type: integer
+ default: 50
+
+ - name: get_ticket
+ description: "Get any ticket details"
+ parameters:
+ - name: ticket_id
+ type: string
+
+ - name: update_ticket
+ description: "Update any ticket"
+ parameters:
+ - name: ticket_id
+ type: string
+ - name: message
+ type: string
+ - name: status
+ type: string
+ required: false
+ - name: assignee
+ type: string
+ required: false
+
+ - name: close_ticket
+ description: "Close any ticket"
+ parameters:
+ - name: ticket_id
+ type: string
+ - name: reason
+ type: string
+ required: false
+
+ - name: create_ticket
+ description: "Create a new ticket on behalf of a user"
+ enabled: true
+ parameters:
+ - name: user_id
+ type: string
+ - name: subject
+ type: string
+ - name: body
+ type: string
+ - name: priority
+ type: string
+ required: false
+
+ - name: list_all_tickets
+ description: "List all tickets with pagination"
+ parameters:
+ - name: page
+ type: integer
+ default: 1
+ - name: per_page
+ type: integer
+ default: 25
+ - name: status
+ type: string
+ required: false
+
+ - name: cost_analytics
+ description: "Get token usage and cost estimates"
+ parameters:
+ - name: period
+ type: string
+ default: "today"
+ - name: group_by
+ type: string
+ default: "session"
+
+ - name: system_health
+ description: "Get system health metrics"
+ parameters: []
+
+ - name: search_knowledge_base
+ description: "Search knowledge base"
+ parameters:
+ - name: query
+ type: string
+ - name: limit
+ type: integer
+ default: 10
+
+ - name: web_search
+ description: "Search the web"
+ parameters:
+ - name: query
+ type: string
+
+ - name: manage_knowledge_base
+ description: "Add/remove knowledge base articles"
+ parameters:
+ - name: action
+ type: string
+ - name: content
+ type: string
+ - name: source
+ type: string
+
+security:
+ rate_limiting:
+ enabled: true
+ max_requests_per_session: 200
+ window_seconds: 3600
+ max_message_length: 8000
+ max_session_duration: 14400
+ auth:
+ required: true
+ api_keys:
+ - ${ADMIN_API_KEY}
+ audit_log:
+ enabled: true
+ log_all_requests: true
+
+session:
+ storage: redis
+ redis_url: ${REDIS_URL}
+ ttl: 14400
+ db: 1
+
+knowledge_base:
+ backend: chroma
+ url: ${CHROMA_URL}
+ collection: helpdesk-kb
+ embedding_model: all-MiniLM-L6-v2
+ manage: true
+
+database:
+ url: ${POSTGRES_URL}
+ pool_size: 10
+ max_overflow: 20
+
+logging:
+ level: INFO
+ format: json
+ file: /app/data/logs/admin-agent.log
diff --git a/config/agent-bridge.yaml b/config/agent-bridge.yaml
new file mode 100644
index 0000000..7f9d6d0
--- /dev/null
+++ b/config/agent-bridge.yaml
@@ -0,0 +1,38 @@
+# ═══════════════════════════════════════════════════
+# Agent Bridge Configuration
+# Allows main Hermes to delegate to helpdesk agent
+# ═══════════════════════════════════════════════════
+
+bridges:
+ helpdesk-agent:
+ url: "http://helpdesk-agent:8080"
+ description: "Customer-facing helpdesk agent (restricted tools)"
+ timeout: 30
+ retry: 2
+ # When main Hermes receives "helpdesk" or "ticket" intent, delegate here
+ triggers:
+ - "ticket"
+ - "helpdesk"
+ - "support"
+ - "my issue"
+ - "my ticket"
+
+ admin-agent:
+ url: "http://admin-agent:8082"
+ description: "Admin agent for helpdesk management"
+ timeout: 60
+ retry: 1
+ auth:
+ header: "X-Admin-Key"
+ key: ${ADMIN_API_KEY}
+ triggers:
+ - "admin"
+ - "manage tickets"
+ - "cost"
+ - "analytics"
+ - "system health"
+
+# How main Hermes should route:
+# 1. Customer asks about ticket → delegate to helpdesk-agent
+# 2. Admin asks for analytics → delegate to admin-agent
+# 3. Unknown → handle locally with general knowledge
diff --git a/config/hermes-config.yaml b/config/hermes-config.yaml
new file mode 100644
index 0000000..bd6cd3b
--- /dev/null
+++ b/config/hermes-config.yaml
@@ -0,0 +1,126 @@
+# ═══════════════════════════════════════════════════
+# Hermes Agent Configuration - Helpdesk Mode
+# ═══════════════════════════════════════════════════
+
+agent:
+ name: "J1 Helpdesk Agent"
+ mode: helpdesk
+ port: 8080
+ description: "Self-hosted AI helpdesk agent for customer support"
+
+llm:
+ provider: openai-compatible
+ api_base: ${LLM_API_BASE}
+ api_key: "not-needed"
+ model: ${LLM_MODEL}
+ context_length: 65536
+ max_tokens: 2048
+ temperature: 0.3
+ top_p: 0.9
+
+tools:
+ # Helpdesk tools (NO create_ticket for end-users)
+ - name: search_tickets
+ description: "Search user's tickets by keyword, status, or date range"
+ parameters:
+ - name: query
+ type: string
+ description: "Search query"
+ - name: status
+ type: string
+ description: "Filter by status (open, pending, closed)"
+ required: false
+ - name: limit
+ type: integer
+ description: "Max results"
+ default: 10
+
+ - name: get_ticket
+ description: "Get details of a specific ticket by ID"
+ parameters:
+ - name: ticket_id
+ type: string
+ description: "Ticket ID"
+
+ - name: update_ticket
+ description: "Add a reply or update own ticket"
+ parameters:
+ - name: ticket_id
+ type: string
+ - name: message
+ type: string
+ description: "Reply message"
+ - name: status
+ type: string
+ description: "New status (open, closed)"
+ required: false
+
+ - name: close_ticket
+ description: "Close own ticket"
+ parameters:
+ - name: ticket_id
+ type: string
+ - name: reason
+ type: string
+ description: "Reason for closing"
+ required: false
+
+ - name: search_knowledge_base
+ description: "Search knowledge base articles"
+ parameters:
+ - name: query
+ type: string
+ - name: limit
+ type: integer
+ default: 5
+
+ - name: web_search
+ description: "Search the web for information"
+ parameters:
+ - name: query
+ type: string
+ - name: limit
+ type: integer
+ default: 5
+
+ - name: create_ticket
+ description: "Create a new ticket (DISABLED in helpdesk mode)"
+ enabled: false
+
+security:
+ rate_limiting:
+ enabled: true
+ max_requests_per_session: ${RATE_LIMIT_PER_SESSION}
+ window_seconds: ${RATE_LIMIT_WINDOW}
+ max_message_length: ${MAX_MESSAGE_LENGTH}
+ max_session_duration: ${MAX_SESSION_DURATION}
+ content_filter:
+ enabled: true
+ block_patterns:
+ - "password"
+ - "credit_card"
+ - "ssn"
+ audit_log:
+ enabled: true
+ log_all_requests: true
+
+session:
+ storage: redis
+ redis_url: ${REDIS_URL}
+ ttl: ${MAX_SESSION_DURATION}
+
+knowledge_base:
+ backend: chroma
+ url: ${CHROMA_URL}
+ collection: helpdesk-kb
+ embedding_model: all-MiniLM-L6-v2
+
+database:
+ url: ${POSTGRES_URL}
+ pool_size: 5
+ max_overflow: 10
+
+logging:
+ level: INFO
+ format: json
+ file: /app/data/logs/agent.log
diff --git a/config/mcp-config.yaml b/config/mcp-config.yaml
new file mode 100644
index 0000000..c2fa06a
--- /dev/null
+++ b/config/mcp-config.yaml
@@ -0,0 +1,75 @@
+# ═══════════════════════════════════════════════════
+# MCP (Model Context Protocol) Client Configuration
+# Connects to external MCP servers (Freshdesk, etc.)
+# ═══════════════════════════════════════════════════
+
+mcp_servers:
+ freshdesk:
+ # Freshdesk MCP Server (NeuraLegion/freshdesk_mcp)
+ # 41 tools for tickets, contacts, companies, KB, etc.
+ transport: stdio
+ command: "node"
+ args:
+ - "/app/mcp-servers/freshdesk/dist/index.js"
+ env:
+ FRESHDESK_DOMAIN: "${FRESHDESK_DOMAIN}"
+ FRESHDESK_API_KEY: "${FRESHDESK_API_KEY}"
+ enabled: true
+ # Which tools to expose to the agent
+ allowed_tools:
+ - list_tickets
+ - view_ticket
+ - create_ticket
+ - search_tickets
+ - update_ticket
+ - list_ticket_conversations
+ - reply_to_ticket
+ - add_note_to_ticket
+ - list_contacts
+ - view_contact
+ - search_contacts
+ - list_companies
+ - view_company
+ - search_solutions
+ - list_solution_categories
+ - list_solution_folders
+ - list_solution_articles
+ - view_solution_article
+
+ # Example: Add more MCP servers here
+ # osticket:
+ # transport: stdio
+ # command: "node"
+ # args: ["/app/mcp-servers/osticket/dist/index.js"]
+ # env:
+ # OSTICKET_URL: "${OSTICKET_URL}"
+ # OSTICKET_API_KEY: "${OSTICKET_API_KEY}"
+ # enabled: false
+
+# MCP Tool Permissions
+# Control which tools each agent mode can use
+permissions:
+ helpdesk:
+ # Helpdesk agent (end-users) — read-only + update own
+ allowed:
+ - list_tickets
+ - view_ticket
+ - search_tickets
+ - list_ticket_conversations
+ - reply_to_ticket
+ - search_solutions
+ - list_solution_categories
+ - list_solution_folders
+ - list_solution_articles
+ - view_solution_article
+ denied:
+ - create_ticket
+ - add_note_to_ticket
+ - update_ticket
+ - list_contacts
+ - list_companies
+
+ admin:
+ # Admin agent — full access to all tools
+ allowed: ["*"]
+ denied: []
diff --git a/config/mcp-registry.yaml b/config/mcp-registry.yaml
new file mode 100644
index 0000000..edf5117
--- /dev/null
+++ b/config/mcp-registry.yaml
@@ -0,0 +1,132 @@
+# ═══════════════════════════════════════════════════
+# MCP Server Registry
+# Pre-configured MCP server definitions for common platforms
+# ═══════════════════════════════════════════════════
+
+# Freshdesk (active — uses NeuraLegion/freshdesk_mcp)
+# 41 tools: tickets, contacts, companies, agents, KB, time tracking, canned responses
+freshdesk:
+ name: "Freshdesk"
+ description: "Full Freshdesk CRM integration via MCP"
+ repository: "NeuraLegion/freshdesk_mcp"
+ transport: stdio
+ command: "node"
+ args: ["/app/mcp-servers/freshdesk/dist/index.js"]
+ env:
+ FRESHDESK_DOMAIN: "${FRESHDESK_DOMAIN}"
+ FRESHDESK_API_KEY: "${FRESHDESK_API_KEY}"
+ tools_total: 41
+ categories:
+ - tickets
+ - contacts
+ - companies
+ - agents
+ - knowledge_base
+ - time_tracking
+ - canned_responses
+ - satisfaction_ratings
+ - system_config
+
+# osTicket (adapter-based — no MCP needed, uses REST API directly)
+# The osticket.py adapter handles all operations
+osticket:
+ name: "osTicket"
+ description: "Ticket management via osTicket REST API"
+ type: adapter
+ adapter_file: "ticket_platforms/osticket.py"
+ config:
+ url_env: "OSTICKET_BASE_URL"
+ key_env: "OSTICKET_API_KEY"
+ capabilities:
+ - create_ticket
+ - search_tickets
+ - update_ticket
+ - list_tickets
+ - add_note
+ - close_ticket
+
+# Zammad (adapter-based — uses REST API)
+zammad:
+ name: "Zammad"
+ description: "Ticket management via Zammad REST API"
+ type: adapter
+ adapter_file: "ticket_platforms/zammad.py"
+ config:
+ url_env: "ZAMMAD_BASE_URL"
+ token_env: "ZAMMAD_API_TOKEN"
+ capabilities:
+ - create_ticket
+ - search_tickets
+ - update_ticket
+ - list_tickets
+ - add_article
+ - close_ticket
+
+# Slack (optional — for notifications and agent communication)
+# Uses modelcontextprotocol/servers/src/slack
+slack:
+ name: "Slack"
+ description: "Send notifications and manage channels"
+ repository: "modelcontextprotocol/servers"
+ transport: stdio
+ command: "npx"
+ args: ["-y", "@modelcontextprotocol/server-slack"]
+ env:
+ SLACK_BOT_TOKEN: "${SLACK_BOT_TOKEN}"
+ SLACK_TEAM_ID: "${SLACK_TEAM_ID}"
+ tools:
+ - send_message
+ - list_channels
+ - list_users
+ - upload_file
+ optional: true
+
+# GitHub (optional — for issue tracking and documentation)
+# Uses github/github-mcp-server
+github:
+ name: "GitHub"
+ description: "Issue tracking, PRs, and repository management"
+ repository: "github/github-mcp-server"
+ transport: https
+ url: "https://api.github.com/mcp"
+ headers:
+ Authorization: "Bearer ${GITHUB_TOKEN}"
+ Accept: "application/json"
+ tools:
+ - create_issue
+ - list_issues
+ - get_issue
+ - create_pull_request
+ - search_repositories
+ - get_file_contents
+ optional: true
+
+# SearXNG (optional MCP — for structured search)
+# Can be used alongside the direct HTTP integration
+searxng:
+ name: "SearXNG"
+ description: "Self-hosted search via MCP"
+ type: http
+ url: "http://searxng:8080/search"
+ format: json
+ optional: true
+
+# PostgreSQL (optional MCP — for direct database queries)
+postgres:
+ name: "PostgreSQL"
+ description: "Database queries and management"
+ repository: "modelcontextprotocol/servers"
+ transport: stdio
+ command: "npx"
+ args: ["-y", "@modelcontextprotocol/server-postgresql"]
+ env:
+ POSTGRES_CONNECTION_STRING: "${POSTGRES_URL}"
+ tools:
+ - query
+ - list_tables
+ - get_table_schema
+ optional: true
+
+# Usage:
+# To enable an MCP server, add its config to config/mcp-config.yaml
+# and set the required environment variables in .env
diff --git a/config/nginx.conf b/config/nginx.conf
new file mode 100644
index 0000000..b4221da
--- /dev/null
+++ b/config/nginx.conf
@@ -0,0 +1,111 @@
+events {
+ worker_connections 1024;
+}
+
+http {
+ # Rate limiting zones
+ limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
+ limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
+ limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
+
+ # Proxy cache
+ proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=hermes_cache:10m max_size=100m inactive=60m;
+
+ # Default upstream
+ upstream helpdesk_agent {
+ server helpdesk-agent:8080;
+ }
+ upstream admin_agent {
+ server admin-agent:8082;
+ }
+ upstream n8n_backend {
+ server n8n:5678;
+ }
+ upstream searxng_backend {
+ server searxng:8080;
+ }
+
+ server {
+ listen 80;
+ server_name _;
+
+ # Security headers
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header X-XSS-Protection "1; mode=block" always;
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+ add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+
+ # Hide server version
+ server_tokens off;
+
+ # Request size limit (match agent config)
+ client_max_body_size 4k;
+ client_body_timeout 30s;
+ client_header_timeout 30s;
+
+ # Helpdesk Agent API
+ location /helpdesk/ {
+ limit_req zone=api burst=20 nodelay;
+ proxy_pass http://helpdesk_agent/;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_cache hermes_cache;
+ proxy_cache_valid 200 5m;
+ }
+
+ # Admin Agent API
+ location /admin/ {
+ limit_req zone=api burst=10 nodelay;
+ # IP whitelist - only allow local network
+ allow 172.20.0.0/16;
+ allow 127.0.0.1;
+ deny all;
+ proxy_pass http://admin_agent/;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ # n8n
+ location /n8n/ {
+ limit_req zone=api burst=20 nodelay;
+ proxy_pass http://n8n_backend/;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ }
+
+ # SearXNG
+ location /search/ {
+ limit_req zone=api burst=30 nodelay;
+ proxy_pass http://searxng_backend/;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ }
+
+ # Admin Dashboard (static)
+ location /dashboard/ {
+ alias /app/admin/;
+ index admin-dashboard.html;
+ autoindex off;
+ }
+
+ # Health check
+ location /health {
+ access_log off;
+ return 200 '{"status":"ok"}';
+ add_header Content-Type application/json;
+ }
+
+ # Default - redirect to dashboard
+ location / {
+ return 302 /dashboard/;
+ }
+ }
+}
diff --git a/config/searxng-settings.yml b/config/searxng-settings.yml
new file mode 100644
index 0000000..0c8ff25
--- /dev/null
+++ b/config/searxng-settings.yml
@@ -0,0 +1,66 @@
+# SearXNG Configuration for Helpdesk Agent
+# Place at /etc/searxng/settings.yml inside container
+
+server:
+ port: 8080
+ bind_address: "0.0.0.0"
+ secret_key: "${SEARX_SECRET_KEY:-change_this_secret_key}"
+ base_url: "${SEARXNG_BASE_URL:-http://localhost:8888/}"
+ image_proxy: true
+
+search:
+ safe_search: 0
+ autocomplete: ""
+ default_lang: "en"
+ formats:
+ - html
+ - json
+ - csv
+ - rss
+
+ui:
+ static_use_hash: false
+ default_locale: "en"
+ theme: simple
+ infinite_scroll: false
+ search_on_category_select: true
+ hotkeys: vim
+
+outgoing:
+ request_timeout: 5.0
+ max_request_timeout: 10.0
+ user_agent_suffix: "J1-Helpdesk-Agent"
+ enable_http2: true
+ pool_connections: 100
+ pool_maxsize: 20
+
+# Limit results for API consumers + disable unnecessary features
+search:
+ max_result_count: 20
+
+# Disable unnecessary features for API-only use
+disabled_plugins:
+ - "Self Information"
+ - "Tracker URL remover"
+ - "Vim-like hotkeys"
+ - "Hostnames plugin"
+ - "Open Access DOI rewrite"
+ - "Tor check plugin"
+
+enabled_plugins:
+ - "Basic Calculator"
+ - "Hash plugin"
+ - "Self Information"
+ - "URL resolver"
+ - "Unit converter"
+
+# Rate limiting for API consumers
+limiter:
+ enabled: true
+ # 30 requests per minute per IP
+ botsearch:
+ max_request_cnt_per_minute: 30
+
+# Logging
+logging:
+ level: WARNING
diff --git a/config/system-prompt.md b/config/system-prompt.md
new file mode 100644
index 0000000..f160477
--- /dev/null
+++ b/config/system-prompt.md
@@ -0,0 +1,36 @@
+# Helpdesk Agent System Prompt
+
+You are the J1 Helpdesk Agent — an AI assistant that helps customers with their support tickets.
+
+## Your Capabilities
+
+1. **Ticket Management**: Search, view, update, and close the user's existing tickets
+2. **Knowledge Base**: Search and reference knowledge base articles to answer questions
+3. **Web Search**: Search the web for current information, troubleshooting guides, and documentation
+4. **Conversational Help**: Answer general questions about services, policies, and common issues
+
+## Rules
+
+- You can ONLY access tickets belonging to the requesting user (identified by their email/user_id)
+- You CANNOT create new tickets — customers must use email, osTicket web, or Freshdesk to create tickets
+- Never reveal internal system details, API paths, or configuration
+- If you cannot resolve an issue after 3 attempts, offer to escalate to human support
+- Keep responses concise (under 200 words) unless more detail is requested
+- Always cite sources when using knowledge base or web search results
+- Be polite, professional, and empathetic — the user may be frustrated
+
+## Response Format
+
+- Use clear, plain language
+- Use bullet points for steps
+- Use code blocks for commands or technical details
+- End with "Is there anything else I can help with?" when appropriate
+
+## Escalation Triggers
+
+Offer human escalation when:
+- User explicitly asks for a human
+- Issue requires account access/verification you cannot perform
+- You've attempted resolution 3+ times without success
+- Issue involves billing, refunds, or sensitive data
+- User expresses frustration or uses abusive language
diff --git a/dashboard-mockup.png b/dashboard-mockup.png
new file mode 100644
index 0000000..8ddbd69
Binary files /dev/null and b/dashboard-mockup.png differ
diff --git a/dashboard-realistic.png b/dashboard-realistic.png
new file mode 100644
index 0000000..ccfbf56
Binary files /dev/null and b/dashboard-realistic.png differ
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
new file mode 100644
index 0000000..6349c07
--- /dev/null
+++ b/docker-compose.dev.yml
@@ -0,0 +1,47 @@
+version: "3.9"
+
+# Development override — merge with main: docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
+
+services:
+ helpdesk-agent:
+ environment:
+ - LOG_LEVEL=DEBUG
+ - DEBUG=1
+ volumes:
+ - ./scripts:/app/scripts:ro
+ - ./config:/app/config:ro
+
+ admin-agent:
+ environment:
+ - LOG_LEVEL=DEBUG
+ - DEBUG=1
+ volumes:
+ - ./scripts:/app/scripts:ro
+ - ./config:/app/config:ro
+
+ llama:
+ ports:
+ - "0.0.0.0:8081:8081" # Expose for development
+
+ whatsapp-webhook:
+ build:
+ context: .
+ dockerfile: Dockerfile.whatsapp
+ ports:
+ - "127.0.0.1:9090:9090"
+ - "0.0.0.0:8383:8383" # WhatsApp HTTPS
+
+ health-monitor:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ volumes:
+ - ./scripts:/app/scripts:ro
+ - ./config:/app/config:ro
+
+ tools-ui:
+ build:
+ context: tools-ui
+ dockerfile: Dockerfile
+ ports:
+ - "127.0.0.1:8484:8484"
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
new file mode 100644
index 0000000..eb1462a
--- /dev/null
+++ b/docker-compose.prod.yml
@@ -0,0 +1,153 @@
+version: "3.9"
+
+# Production override — use with: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
+
+services:
+ llama:
+ restart: always
+ deploy:
+ resources:
+ limits:
+ memory: 7G
+ cpus: "6.0"
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://127.0.0.1:8081/health"]
+ interval: 15s
+ timeout: 5s
+ retries: 10
+ start_period: 120s
+
+ helpdesk-agent:
+ restart: always
+ environment:
+ - LOG_LEVEL=WARNING
+ - DEBUG=0
+ deploy:
+ resources:
+ limits:
+ memory: 2G
+ cpus: "2.0"
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://127.0.0.1:8080/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+
+ admin-agent:
+ restart: always
+ environment:
+ - LOG_LEVEL=WARNING
+ - DEBUG=0
+ deploy:
+ resources:
+ limits:
+ memory: 2G
+ cpus: "2.0"
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://127.0.0.1:8082/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+
+ whatsapp-webhook:
+ restart: always
+ environment:
+ - LOG_LEVEL=WARNING
+ deploy:
+ resources:
+ limits:
+ memory: 500M
+ cpus: "1.0"
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://127.0.0.1:9090/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+
+ chroma:
+ restart: always
+ deploy:
+ resources:
+ limits:
+ memory: 2G
+ cpus: "2.0"
+
+ postgres:
+ restart: always
+ deploy:
+ resources:
+ limits:
+ memory: 1G
+ cpus: "1.0"
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U helpdesk"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+ redis:
+ restart: always
+ command: >
+ redis-server
+ --requirepass ${REDIS_PASSWORD:-redis_pass}
+ --maxmemory 256mb
+ --maxmemory-policy allkeys-lru
+ --appendonly yes
+ --appendfsync everysec
+ --save 60 1000
+ --save 300 100
+ deploy:
+ resources:
+ limits:
+ memory: 300M
+
+ searxng:
+ restart: always
+ deploy:
+ resources:
+ limits:
+ memory: 1G
+
+ n8n:
+ restart: always
+ environment:
+ - N8N_RUNNERS_ENABLED=true
+ deploy:
+ resources:
+ limits:
+ memory: 1G
+
+ nginx:
+ restart: always
+ ports:
+ - "80:80"
+ - "443:443"
+ deploy:
+ resources:
+ limits:
+ memory: 200M
+
+ health-monitor:
+ restart: always
+ environment:
+ - LOG_LEVEL=INFO
+ deploy:
+ resources:
+ limits:
+ memory: 200M
+
+ email-fetcher:
+ restart: always
+ environment:
+ - LOG_LEVEL=INFO
+ deploy:
+ resources:
+ limits:
+ memory: 500M
+
+ tools-ui:
+ restart: always
+ deploy:
+ resources:
+ limits:
+ memory: 100M
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..ebf3107
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,450 @@
+version: "3.9"
+
+services:
+ # ═══════════════════════════════════════════════════
+ # LLM Inference (llama.cpp with Qwen2.5-7B)
+ # ═══════════════════════════════════════════════════
+ llama:
+ image: ghcr.io/ggerganov/llama.cpp:server
+ container_name: helpdesk-llama
+ ports:
+ - "127.0.0.1:8081:8081"
+ volumes:
+ - ./models:/models:ro
+ environment:
+ - LLAMA_ARG_MODEL=/models/qwen2.5-7b-instruct-q4_k_m.gguf
+ - LLAMA_ARG_PORT=8081
+ - LLAMA_ARG_CTX_SIZE=65536
+ - LLAMA_ARG_N_BATCH=512
+ - LLAMA_ARG_THREADS=6
+ - LLAMA_ARG_HOST=0.0.0.0
+ - LLAMA_ARG_N_GL=0
+ deploy:
+ resources:
+ limits:
+ memory: 7G
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://127.0.0.1:8081/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 5
+ start_period: 60s
+ networks:
+ - helpdesk-net
+
+ # ═══════════════════════════════════════════════════
+ # Helpdesk Agent (Hermes - restricted, no ticket creation)
+ # ═══════════════════════════════════════════════════
+ helpdesk-agent:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ container_name: helpdesk-agent
+ ports:
+ - "127.0.0.1:8080:8080"
+ volumes:
+ - ./config:/app/config:ro
+ - ./ticket_platforms:/app/ticket_platforms:ro
+ - ./scripts:/app/scripts:ro
+ - ./knowledge-base:/app/knowledge-base:ro
+ - hermes-data:/app/data
+ environment:
+ - HERMES_CONFIG=/app/config/hermes-config.yaml
+ - LLM_API_BASE=http://llama:8081/v1
+ - LLM_MODEL=qwen2.5-7b-instruct
+ - CHROMA_URL=http://chroma:8000
+ - SEARX_URL=http://searxng:8080
+ - POSTGRES_URL=postgresql://helpdesk:${DB_PASSWORD:-helpdesk_pass}@postgres:5432/helpdesk
+ - REDIS_URL=redis://:${REDIS_PASSWORD:-redis_pass}@redis:6379/0
+ - RATE_LIMIT_PER_SESSION=${RATE_LIMIT_PER_SESSION:-50}
+ - RATE_LIMIT_WINDOW=${RATE_LIMIT_WINDOW:-3600}
+ - MAX_MESSAGE_LENGTH=${MAX_MESSAGE_LENGTH:-4000}
+ - MAX_SESSION_DURATION=${MAX_SESSION_DURATION:-7200}
+ - AGENT_MODE=helpdesk
+ - ALLOW_CREATE_TICKET=false
+ - TZ=UTC
+ depends_on:
+ llama:
+ condition: service_healthy
+ chroma:
+ condition: service_started
+ postgres:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+ deploy:
+ resources:
+ limits:
+ memory: 2G
+ restart: unless-stopped
+ networks:
+ - helpdesk-net
+
+ # ═══════════════════════════════════════════════════
+ # Admin Agent (Hermes - full access, plug-in to main Hermes)
+ # ═══════════════════════════════════════════════════
+ admin-agent:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ container_name: helpdesk-admin-agent
+ ports:
+ - "127.0.0.1:8082:8082"
+ volumes:
+ - ./config:/app/config:ro
+ - ./ticket_platforms:/app/ticket_platforms:ro
+ - ./scripts:/app/scripts:ro
+ - ./knowledge-base:/app/knowledge-base:ro
+ - hermes-admin-data:/app/data
+ environment:
+ - HERMES_CONFIG=/app/config/admin-agent-config.yaml
+ - LLM_API_BASE=http://llama:8081/v1
+ - LLM_MODEL=qwen2.5-7b-instruct
+ - CHROMA_URL=http://chroma:8000
+ - SEARX_URL=http://searxng:8080
+ - POSTGRES_URL=postgresql://helpdesk:${DB_PASSWORD:-helpdesk_pass}@postgres:5432/helpdesk
+ - REDIS_URL=redis://:${REDIS_PASSWORD:-redis_pass}@redis:6379/1
+ - AGENT_MODE=admin
+ - ALLOW_CREATE_TICKET=true
+ - TZ=UTC
+ depends_on:
+ llama:
+ condition: service_healthy
+ chroma:
+ condition: service_started
+ postgres:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+ deploy:
+ resources:
+ limits:
+ memory: 2G
+ restart: unless-stopped
+ networks:
+ - helpdesk-net
+
+ # ═══════════════════════════════════════════════════
+ # ChromaDB (Knowledge Base Vector Store)
+ # ═══════════════════════════════════════════════════
+ chroma:
+ image: chromadb/chroma:0.5.23
+ container_name: helpdesk-chroma
+ ports:
+ - "127.0.0.1:8000:8000"
+ volumes:
+ - chroma-data:/chroma/chroma
+ environment:
+ - CHROMA_SERVER_HOST=0.0.0.0
+ - CHROMA_SERVER_PORT=8000
+ - PERSIST_DIRECTORY=/chroma/chroma
+ - ALLOW_RESET=true
+ - CHROMA_AUTH_TOKEN=${CHROMA_AUTH_TOKEN:-chromadb_token_change_me}
+ deploy:
+ resources:
+ limits:
+ memory: 2G
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "curl", "-sf", "http://127.0.0.1:8000/api/v1/heartbeat"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ networks:
+ - helpdesk-net
+
+ # ═══════════════════════════════════════════════════
+ # SearXNG (Self-hosted Web Search)
+ # ═══════════════════════════════════════════════════
+ searxng:
+ image: searxng/searxng:latest
+ container_name: helpdesk-searxng
+ ports:
+ - "127.0.0.1:8888:8080"
+ volumes:
+ - searxng-data:/etc/searxng
+ - ./config/searxng-settings.yml:/etc/searxng/settings.yml:ro
+ environment:
+ - SEARXNG_BASE_URL=http://localhost:8888/
+ - SEARXNG_BIND_ADDRESS=0.0.0.0
+ deploy:
+ resources:
+ limits:
+ memory: 1G
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "curl", "-sf", "http://127.0.0.1:8888/search?q=test&format=json"]
+ interval: 60s
+ timeout: 10s
+ retries: 3
+ networks:
+ - helpdesk-net
+
+ # ═══════════════════════════════════════════════════
+ # n8n (Workflow Automation)
+ # ═══════════════════════════════════════════════════
+ n8n:
+ image: n8nio/n8n:latest
+ container_name: helpdesk-n8n
+ ports:
+ - "127.0.0.1:5678:5678"
+ volumes:
+ - n8n-data:/home/node/.n8n
+ - ./workflows:/home/node/.n8n/workflows:ro
+ environment:
+ - N8N_HOST=localhost
+ - N8N_PORT=5678
+ - N8N_PROTOCOL=http
+ - WEBHOOK_URL=http://localhost:5678/
+ - GENERIC_TIMEZONE=UTC
+ - N8N_CUSTOM_ENDPOINTS=webhook
+ - N8N_USER_MANAGEMENT_JWT_SECRET=${JWT_SECRET:-jwt_secret_change_me_please}
+ - N8N_DEFAULT_BINARY_DATA_MODE=filesystem
+ deploy:
+ resources:
+ limits:
+ memory: 1G
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "curl", "-sf", "http://127.0.0.1:5678/healthz"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ networks:
+ - helpdesk-net
+
+ # ═══════════════════════════════════════════════════
+ # PostgreSQL (Ticket Database + Agent State)
+ # ═══════════════════════════════════════════════════
+ postgres:
+ image: postgres:16-alpine
+ container_name: helpdesk-postgres
+ ports:
+ - "127.0.0.1:5432:5432"
+ volumes:
+ - postgres-data:/var/lib/postgresql/data
+ - ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql:ro
+ environment:
+ - POSTGRES_DB=helpdesk
+ - POSTGRES_USER=helpdesk
+ - POSTGRES_PASSWORD=${DB_PASSWORD:-helpdesk_pass}
+ - POSTGRES_INITDB_ARGS=--encoding=UTF8 --lc-collate=C --lc-ctype=C
+ deploy:
+ resources:
+ limits:
+ memory: 1G
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U helpdesk"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ networks:
+ - helpdesk-net
+
+ # ═══════════════════════════════════════════════════
+ # Redis (Caching + Session Store + Rate Limiting)
+ # ═══════════════════════════════════════════════════
+ redis:
+ image: redis:7-alpine
+ container_name: helpdesk-redis
+ ports:
+ - "127.0.0.1:6379:6379"
+ volumes:
+ - redis-data:/data
+ command: >
+ redis-server
+ --requirepass ${REDIS_PASSWORD:-redis_pass}
+ --maxmemory 256mb
+ --maxmemory-policy allkeys-lru
+ --appendonly yes
+ --appendfsync everysec
+ deploy:
+ resources:
+ limits:
+ memory: 300M
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-redis_pass}", "ping"]
+ interval: 10s
+ timeout: 5s
+ retries: 3
+ networks:
+ - helpdesk-net
+
+ # ═══════════════════════════════════════════════════
+ # Nginx (Reverse Proxy + Security Headers + Rate Limiting)
+ # ═══════════════════════════════════════════════════
+ nginx:
+ image: nginx:alpine
+ container_name: helpdesk-nginx
+ ports:
+ - "80:80"
+ - "443:443"
+ volumes:
+ - ./config/nginx.conf:/etc/nginx/nginx.conf:ro
+ - ./admin:/app/admin:ro
+ - ./certs:/etc/nginx/certs:ro
+ - nginx-cache:/var/cache/nginx
+ depends_on:
+ - helpdesk-agent
+ - admin-agent
+ - n8n
+ - searxng
+ deploy:
+ resources:
+ limits:
+ memory: 200M
+ restart: unless-stopped
+ networks:
+ - helpdesk-net
+
+ # ═══════════════════════════════════════════════════
+ # Email Fetcher (IMAP → Ticket via helpdesk-agent)
+ # ═══════════════════════════════════════════════════
+ email-fetcher:
+ build:
+ context: .
+ dockerfile: Dockerfile.email
+ container_name: helpdesk-email-fetcher
+ volumes:
+ - ./config:/app/config:ro
+ - ./ticket_platforms:/app/ticket_platforms:ro
+ - email-queue:/app/queue
+ environment:
+ - IMAP_HOST=${IMAP_HOST:-}
+ - IMAP_PORT=${IMAP_PORT:-993}
+ - IMAP_USER=${IMAP_USER:-}
+ - IMAP_PASSWORD=${IMAP_PASSWORD:-}
+ - IMAP_FOLDER=INBOX
+ - POLL_INTERVAL=${POLL_INTERVAL:-60}
+ - TICKET_PLATFORM=${TICKET_PLATFORM:-osticket}
+ - HELPDESK_AGENT_URL=http://helpdesk-agent:8080
+ - POSTGRES_URL=postgresql://helpdesk:${DB_PASSWORD:-helpdesk_pass}@postgres:5432/helpdesk
+ - TZ=UTC
+ depends_on:
+ helpdesk-agent:
+ condition: service_started
+ postgres:
+ condition: service_healthy
+ deploy:
+ resources:
+ limits:
+ memory: 500M
+ restart: unless-stopped
+ networks:
+ - helpdesk-net
+
+# ═══════════════════════════════════════════════════
+# WhatsApp Webhook Receiver
+# ═══════════════════════════════════════════════════
+whatsapp-webhook:
+ build:
+ context: .
+ dockerfile: Dockerfile.whatsapp
+ container_name: helpdesk-whatsapp
+ ports:
+ - "127.0.0.1:9090:9090"
+ - "0.0.0.0:8383:8383"
+ volumes:
+ - ./config:/app/config:ro
+ - ./scripts:/app/scripts:ro
+ environment:
+ - WHATSAPP_TOKEN=${WHATSAPP_TOKEN}
+ - WHATSAPP_PHONE_NUMBER_ID=${WHATSAPP_PHONE_NUMBER_ID}
+ - WHATSAPP_WEBHOOK_SECRET=${WHATSAPP_WEBHOOK_SECRET:-change_me}
+ - ADMIN_PHONE_NUMBER=${ADMIN_PHONE_NUMBER}
+ - HELPDESK_AGENT_URL=http://helpdesk-agent:8080
+ - REDIS_URL=redis://:${REDIS_PASSWORD:-redis_pass}@redis:6379/0
+ - WHATSAPP_RATE_LIMIT_PER_MINUTE=${WHATSAPP_RATE_LIMIT_PER_MINUTE:-10}
+ depends_on:
+ helpdesk-agent:
+ condition: service_started
+ redis:
+ condition: service_healthy
+ deploy:
+ resources:
+ limits:
+ memory: 500M
+ restart: unless-stopped
+ networks:
+ - helpdesk-net
+
+# ═══════════════════════════════════════════════════
+# Health Monitor
+# ═══════════════════════════════════════════════════
+health-monitor:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ container_name: helpdesk-health
+ volumes:
+ - ./scripts:/app/scripts:ro
+ - ./config:/app/config:ro
+ environment:
+ - REDIS_URL=redis://:${REDIS_PASSWORD:-redis_pass}@redis:6379/0
+ depends_on:
+ redis:
+ condition: service_healthy
+ deploy:
+ resources:
+ limits:
+ memory: 200M
+ restart: unless-stopped
+ networks:
+ - helpdesk-net
+
+# ═══════════════════════════════════════════════════
+# Tools UI (Widget + Admin Panel)
+# ═══════════════════════════════════════════════════
+tools-ui:
+ build:
+ context: tools-ui
+ dockerfile: Dockerfile
+ container_name: helpdesk-tools-ui
+ ports:
+ - "127.0.0.1:8484:8484"
+ depends_on:
+ - helpdesk-agent
+ - admin-agent
+ deploy:
+ resources:
+ limits:
+ memory: 200M
+ restart: unless-stopped
+ networks:
+ - helpdesk-net
+
+# ═══════════════════════════════════════════════════
+# Networks
+# ═══════════════════════════════════════════════════
+networks:
+ helpdesk-net:
+ driver: bridge
+ ipam:
+ config:
+ - subnet: 172.20.0.0/16
+
+# ═══════════════════════════════════════════════════
+# Volumes
+# ═══════════════════════════════════════════════════
+volumes:
+ hermes-data:
+ driver: local
+ hermes-admin-data:
+ driver: local
+ chroma-data:
+ driver: local
+ searxng-data:
+ driver: local
+ n8n-data:
+ driver: local
+ postgres-data:
+ driver: local
+ redis-data:
+ driver: local
+ nginx-cache:
+ driver: local
+ email-queue:
+ driver: local
diff --git a/helpdesk-agent-diagram-guide.html b/helpdesk-agent-diagram-guide.html
index a8635f7..974714f 100644
--- a/helpdesk-agent-diagram-guide.html
+++ b/helpdesk-agent-diagram-guide.html
@@ -24,8 +24,6 @@
h2 { color: var(--cyan); font-size: 1.4rem; margin-top: 28px; border-bottom: 1px solid var(--border); padding-bottom: 6px; }
h3 { color: var(--yellow); font-size: 1.1rem; margin-top: 20px; }
.lead { color: #9aa3ad; font-size: 1.05rem; margin-bottom: 22px; }
-
- /* Diagram */
.diagram-wrap { background: var(--panel); border: 1px solid var(--border); border-radius: 14px; padding: 22px; margin: 22px 0; overflow-x: auto; }
.layer { display: flex; justify-content: center; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
.layer-label { width: 100%; text-align: center; color: var(--cyan-dim); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.12em; margin: 6px 0 2px; }
@@ -39,8 +37,6 @@
.node.channel strong { color: var(--cyan); }
.node .note { font-size: 0.75rem; color: #8b95a0; margin-top: 6px; }
.arrow { width: 2px; height: 14px; background: #2a2f3a; margin: 0 auto; }
-
- /* Panels */
.panel { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 18px; margin: 14px 0; }
.panel .meta { color: var(--cyan-dim); font-size: 0.8rem; margin-top: 6px; }
code { background: #0b0c10; padding: 2px 6px; border-radius: 4px; color: var(--yellow); font-size: 0.9em; }
@@ -54,9 +50,6 @@
.nav { display: flex; gap: 10px; flex-wrap: wrap; margin: 14px 0; }
.nav a { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px; font-size: 0.88rem; }
.nav a:hover { border-color: var(--cyan); }
- .collapse { border: 1px solid var(--border); border-radius: 10px; padding: 10px 14px; margin: 10px 0; background: var(--panel); cursor: pointer; }
- .collapse + .hidden { display: none; }
- .hidden.open { display: block; }
.callout { border-left: 3px solid var(--yellow); padding: 10px 14px; background: #14202b; border-radius: 0 8px 8px 0; margin: 12px 0; }
.kbd { background: #0b0c10; border: 1px solid var(--border); padding: 2px 6px; border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Ubuntu Mono, monospace; }
table { width: 100%; border-collapse: collapse; margin: 12px 0; }
@@ -75,9 +68,10 @@
🔧 Self-Hosted Help Desk Agent
VM Setup
Architecture Diagram
Stack
-
osTicket
+
Ticket Platforms
Memory
Model
+
Admin Dashboard
Security
Operations
Checklist
@@ -120,6 +114,7 @@
🔧 Self-Hosted Help Desk Agent
Telegram BotFather bot / webhook
WhatsApp baileys / whatsapp-web.js session
iMessage macOS relay recommended
+
Email IMAP poller → ticket
@@ -133,7 +128,7 @@ 🔧 Self-Hosted Help Desk Agent
SearXNG Self-hosted web search
KB / Wiki Markdown docs, how-tos
-
Ticketing SQLite-native or Zammad/osTicket API
+
Ticketing osTicket / Zammad / email via registry
Memory Conversation + ticket state
@@ -152,32 +147,39 @@ 🔧 Self-Hosted Help Desk Agent
Area Recommended default Alternative
Messaging Telegram first Whatsapp via baileys (higher ops risk)
- Ticketing Hermes-native SQLite “ticket tool” Zammad or osTicket via API
+ Ticketing Adapter registry: osTicket, Zammad, email Add new backends without changing Hermes core
Search SearXNG Single DuckDuckGo scraper (weaker)
KB Local markdown + ripgrep/Whoosh Static web wiki
- Model runtime Remote API or small quantized model Local LLM (heavy CPU)
+ Model runtime llama.cpp local 64k Remote API or small quantized model
+ Admin Self-hosted dashboard External logging SaaS
Auth/Rate limit Allowlist + per-user rate limiting OTP / invite codes
-
-
osTicket Integration
-
Wire osTicket into Hermes via its REST API.
-
# Hermes config
-tools:
- osticket:
- base_url: "https://helpdesk.your-domain.com"
- api_key: "YOUR_OSTICKET_API_KEY"
- default_dept_id: 1
- default_priority: "low"
- rate_limit_per_min: 5
-
Tool calls
-
- create_ticket(user, issue, priority, channel)
- update_ticket(ticket_id, status, note)
- search_tickets(query)
- close_ticket(ticket_id)
-
+
+
+
Admin Dashboard — Live Cost Tracker
+
Shows what this stack would cost if replaced by hosted SaaS equivalents. Open admin/admin-dashboard.html.
+
What admin sees
+
+ Widget Data
+ Tickets Open / closed counts
+ Sessions Unique users / conversations
+ Avg response End-to-end bot latency
+ Monthly / yearly saved Live vs hosted baseline
+
+
Pricing basis
+
+ Hosted equivalent Monthly estimate
+ LLM inference (64k) ~$120–$240
+ Web search API ~$25–$50
+ Embeddings / memory ~$3–$8
+ Telegram / WhatsApp bridge SaaS ~$80–$160
+ Hosted ticketing ~$60–$150
+ Total ~$288–$608 / month
+ Self-hosted software $0
+ Net saved ~$300–$600 / month
+
+
Cost model formula
+
def compute_monthly_savings(prompts, completions, searches, agents=1):
+ llm = (prompts * 2.5 + completions * 10) / 1_000_000
+ search = searches * 0.005
+ seats = agents * 29
+ hosted = llm + search + seats + 80
+ return max(hosted, 0)
+
How to run
+
cd /home//helpdesk-agent/admin
+python3 -m http.server 8081
+# Open http://localhost:8081/admin-dashboard.html
+
+
Security Model
This agent will be public-facing. Treat it like a shared service from day one.
@@ -272,15 +310,31 @@
Hermes config for llama.cpp
Runbook Essentials
-
Onboarding
-
# create venv
-python3.11 -m venv ~/hermes-venv
-source ~/hermes-venv/bin/activate
-
-# install Hermes
-git clone https://github.com/NousResearch/Hermes.git
-cd Hermes
-pip install -e .
+
Ticket adapter registry
+
from ticket_platforms.registry import get
+platform = get("osticket")
+platform.create_ticket(...)
+
+
+
Email ingestion
+
from ticket_platforms.email import EmailTicketAdapter
+adapter = EmailTicketAdapter(
+ imap_host="imap.example.com",
+ imap_port=993,
+ username="...",
+ password="...",
+ ticket_platform="osticket",
+)
+adapter.sync_unread()
+
+
+
+
+
Stack services
+
docker compose -f compose/docker-compose.yml up -d
+docker compose -f compose/docker-compose.selfhosted.yml up -d
+docker compose -f compose/docker-compose.knowledge.yml up -d
+docker compose -f compose/docker-compose.wiki.yml up -d
Systemd
@@ -304,9 +358,10 @@
Systemd