From 10ee0ff461d2b6da1158cc86f72aec0309f583df Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:11:28 -0400 Subject: [PATCH 1/6] build(deps): bump requests from 2.32.3 to 2.33.0 Bumps [requests](https://github.com/psf/requests) from 2.32.3 to 2.33.0. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.32.3...v2.33.0) --- updated-dependencies: - dependency-name: requests dependency-version: 2.33.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- compose/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/requirements.txt b/compose/requirements.txt index c27df4b..94cf104 100644 --- a/compose/requirements.txt +++ b/compose/requirements.txt @@ -1,4 +1,4 @@ -requests==2.32.3 +requests==2.33.0 fastapi==0.111.0 uvicorn==0.30.0 pydantic==2.7.0 From da1791b666b50ce278a8ce8b4a201e3153d23a8d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:11:32 -0400 Subject: [PATCH 2/6] build(deps): bump python-multipart from 0.0.20 to 0.0.31 Bumps [python-multipart](https://github.com/Kludex/python-multipart) from 0.0.20 to 0.0.31. - [Release notes](https://github.com/Kludex/python-multipart/releases) - [Changelog](https://github.com/Kludex/python-multipart/blob/main/CHANGELOG.md) - [Commits](https://github.com/Kludex/python-multipart/compare/0.0.20...0.0.31) --- updated-dependencies: - dependency-name: python-multipart dependency-version: 0.0.31 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1568f93..2bdc7b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ redis==5.2.1 asyncpg==0.30.0 psycopg2-binary==2.9.10 httpx==0.28.1 -python-multipart==0.0.20 +python-multipart==0.0.31 # LLM / OpenAI-compatible client openai==1.59.0 From c2b32f18e3ad51e2c0bb0bdc6bfab1b684578e89 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:11:36 -0400 Subject: [PATCH 3/6] build(deps): bump python-dotenv from 1.0.1 to 1.2.2 Bumps [python-dotenv](https://github.com/theskumar/python-dotenv) from 1.0.1 to 1.2.2. - [Release notes](https://github.com/theskumar/python-dotenv/releases) - [Changelog](https://github.com/theskumar/python-dotenv/blob/main/CHANGELOG.md) - [Commits](https://github.com/theskumar/python-dotenv/compare/v1.0.1...v1.2.2) --- updated-dependencies: - dependency-name: python-dotenv dependency-version: 1.2.2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2bdc7b0..b07d850 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,5 +19,5 @@ chromadb==0.5.23 imaplib2==3.6 # Utilities -python-dotenv==1.0.1 +python-dotenv==1.2.2 uuid6==2024.7.10 From 229c3e4d168e589b1110ad0836bad45f450419c6 Mon Sep 17 00:00:00 2001 From: Villon Date: Sat, 4 Jul 2026 18:26:49 +0000 Subject: [PATCH 4/6] docs: comprehensive README with features, quick start, architecture, and integrations --- README.md | 156 ++++++++++++++++++++---------------------------------- 1 file changed, 58 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index 03026fc..7090311 100644 --- a/README.md +++ b/README.md @@ -1,120 +1,80 @@ -# CommandDesk (CommandDesk) - -**Version:** v0.1 -**Status:** Active Development -**Repository:** https://github.com/OneByJorah/CommandDesk - ---- - -## Table of Contents - -- [Overview](#overview) -- [Architecture](#architecture) -- [Technology Stack](#technology-stack) -- [Features](#features) -- [Getting Started](#getting-started) -- [Service Management](#service-management) -- [Project Structure](#project-structure) -- [Screenshots](#screenshots) -- [Contributing](#contributing) -- [License](#license) -- [Author](#author) - ---- - -## Overview - -Helpdesk and NOC operations dashboard with ticket tracking and system monitoring. - ---- - -## Architecture - -Client โ†’ Local service (`CommandDesk`) โ†’ data/processing modules โ†’ output/api layer. -Secrets and environment configuration are managed via environment files with restrictive permissions. - ---- - -## Technology Stack - -|| Layer | Stack | -|---|---| -| Runtime | Linux (Ubuntu 22.04+) | -| Primary Stack | HTML5 / Python / systemd | -| VCS | Git + GitHub (`github.com/OneByJorah/CommandDesk`) | -| Dev Port | Localhost / systemd service | +
+ + + + +
+ +
+ +
+

๐ŸŽซ CommandDesk

+

Self-Hosted AI Helpdesk Agent

+

100% local, free AI-powered helpdesk with multi-platform ticketing, knowledge base, and multi-channel communication

+

+ Features โ€ข + Quick Start โ€ข + Architecture โ€ข + Integrations +

+
--- -## Features +## โœจ Features -- Operational dashboard and monitoring (per repo). -- Exportable data / reports where supported. -- Extensible service-based design. -- Dark-themed UI where applicable. +- **AI-Powered Ticketing** โ€” Auto-respond, triage, and resolve tickets via AI +- **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 workflows +- **Plug-in Architecture** โ€” Extend with custom adapters and tools ---- - -## Getting Started +## ๐Ÿš€ Quick Start ```bash -# 1. Clone the repository git clone https://github.com/OneByJorah/CommandDesk.git cd CommandDesk - -# 2. Install dependencies -# (see specific subproject docs) - -# 3. Start the service -# (see Service Management below) -``` - ---- - -## Service Management - -```bash -# Start the service (example) -sudo systemctl start CommandDesk.service -sudo systemctl enable CommandDesk.service +docker-compose up -d ``` -Access the service via your configured localhost port or reverse proxy. - ---- - -## Project Structure +## ๐Ÿ—๏ธ Architecture ``` CommandDesk/ -โ”œโ”€โ”€ README.md -โ”œโ”€โ”€ (additional project files) +โ”œโ”€โ”€ admin/ # Admin dashboard +โ”œโ”€โ”€ compose/ # Docker Compose configs +โ”œโ”€โ”€ config/ # Application configuration +โ”œโ”€โ”€ scripts/ # Utility scripts +โ”œโ”€โ”€ skills/ # AI agent skills +โ”œโ”€โ”€ ticket_platforms/ # osTicket, Freshdesk, Zammad +โ”œโ”€โ”€ tools-ui/ # Web UI components +โ”œโ”€โ”€ workflows/ # n8n workflow definitions +โ”œโ”€โ”€ Dockerfile # Backend Docker image +โ”œโ”€โ”€ docker-compose.yml # Main deployment +โ””โ”€โ”€ requirements.txt # Python dependencies ``` ---- - -## Screenshots - -All screenshots are live captures from the local dev instance. - -_(Screenshots will be added after build/run capture.)_ - ---- - -## Contributing +## ๐Ÿ“ก Integrations -1. Create a feature branch off `main`. -2. Follow the existing code style. -3. Submit a PR with description and screenshots for UI changes. +| 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 | +| Email | Channel | IMAP email-to-ticket | +| ChromaDB | Knowledge | Vector search for KB | ---- +## ๐Ÿ“„ License -## License - -MIT +MIT ยฉ Jhonattan L. Jimenez --- -## Author - -Built by **Jhonattan L. Jimenez**. +
+

๐Ÿค– Your AI helpdesk, fully self-hosted

+

@OneByJorah

+
\ No newline at end of file From a4f51e8ce98d6e4d7263c9eca6f963c2ae84539c Mon Sep 17 00:00:00 2001 From: JorahOne Admin Date: Sat, 4 Jul 2026 23:42:42 +0000 Subject: [PATCH 5/6] Apply ruff auto-fixes and portfolio standardization --- .github/ISSUE_TEMPLATE/bug_report.md | 32 ++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 +++++ .github/PULL_REQUEST_TEMPLATE.md | 23 ++++++ .github/dependabot.yml | 21 +++++ .github/workflows/codeql.yml | 37 +++++++++ CODE_OF_CONDUCT.md | 48 +++++++++++ CONTRIBUTING.md | 57 +++++++++++++ LICENSE | 21 +++++ README.md | 99 +++++++++++++++++------ SECURITY.md | 34 ++++++++ memory_setup.py | 4 +- osticket_tool.py | 16 ++-- scripts/agent_server.py | 16 ++-- scripts/analytics.py | 3 +- scripts/email_fetcher.py | 4 +- scripts/health_monitor.py | 3 +- scripts/index_kb.py | 19 +++-- scripts/rate_limiter.py | 5 +- scripts/session_manager.py | 8 +- scripts/whatsapp_webhook.py | 47 ++++++----- ticket_platforms/__init__.py | 6 +- ticket_platforms/base.py | 26 +++--- ticket_platforms/email.py | 9 +-- ticket_platforms/freshdesk.py | 39 +++++---- ticket_platforms/osticket.py | 8 +- ticket_platforms/registry.py | 9 +-- ticket_platforms/zammad.py | 9 +-- 27 files changed, 473 insertions(+), 150 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 SECURITY.md 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/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/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..256974a --- /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 j1admin@onebyjorah.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/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/README.md b/README.md index 7090311..68752fb 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ +
@@ -10,7 +11,7 @@

๐ŸŽซ CommandDesk

Self-Hosted AI Helpdesk Agent

-

100% local, free AI-powered helpdesk with multi-platform ticketing, knowledge base, and multi-channel communication

+

100% local, AI-powered helpdesk with multi-platform ticketing, knowledge base, and multi-channel communication

Features โ€ข Quick Start โ€ข @@ -23,13 +24,13 @@ ## โœจ Features -- **AI-Powered Ticketing** โ€” Auto-respond, triage, and resolve tickets via AI +- **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 workflows +- **Workflow Automation** โ€” n8n integration for complex automation - **Plug-in Architecture** โ€” Extend with custom adapters and tools ## ๐Ÿš€ Quick Start @@ -37,36 +38,88 @@ ```bash git clone https://github.com/OneByJorah/CommandDesk.git cd CommandDesk -docker-compose up -d +cp .env.example .env +# Edit .env with your configuration +docker compose up -d ``` ## ๐Ÿ—๏ธ Architecture ``` -CommandDesk/ -โ”œโ”€โ”€ admin/ # Admin dashboard -โ”œโ”€โ”€ compose/ # Docker Compose configs -โ”œโ”€โ”€ config/ # Application configuration -โ”œโ”€โ”€ scripts/ # Utility scripts -โ”œโ”€โ”€ skills/ # AI agent skills -โ”œโ”€โ”€ ticket_platforms/ # osTicket, Freshdesk, Zammad -โ”œโ”€โ”€ tools-ui/ # Web UI components -โ”œโ”€โ”€ workflows/ # n8n workflow definitions -โ”œโ”€โ”€ Dockerfile # Backend Docker image -โ”œโ”€โ”€ docker-compose.yml # Main deployment -โ””โ”€โ”€ requirements.txt # Python dependencies +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 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 | -| Email | Channel | IMAP email-to-ticket | -| ChromaDB | Knowledge | Vector search for KB | +| **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 @@ -75,6 +128,6 @@ MIT ยฉ Jhonattan L. Jimenez ---

-

๐Ÿค– Your AI helpdesk, fully self-hosted

+

๐Ÿค– AI-powered helpdesk, fully self-hosted

@OneByJorah

\ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..be4cad0 --- /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 **j1admin@onebyjorah.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/memory_setup.py b/memory_setup.py index 8e19ce1..143c075 100644 --- a/memory_setup.py +++ b/memory_setup.py @@ -3,10 +3,10 @@ Assumes llama-server is running on 127.0.0.1:8080 with --embedding enabled. """ +import os import sqlite3 + import requests -import os -import time LLAMA_HOST = os.environ.get("LLAMA_HOST", "http://127.0.0.1:8080") EMBED_MODEL = os.environ.get("LLAMA_EMBED_MODEL", "qwen2.5-7b-instruct-q4_k_m") diff --git a/osticket_tool.py b/osticket_tool.py index cd543fe..5c37b3e 100644 --- a/osticket_tool.py +++ b/osticket_tool.py @@ -6,8 +6,8 @@ import os import re + import requests -from typing import Optional OSTICKET_BASE_URL = os.environ.get("OSTICKET_BASE_URL", "").rstrip("/") OSTICKET_API_KEY = os.environ.get("OSTICKET_API_KEY", "") @@ -48,11 +48,11 @@ def create_ticket( subject: str, body: str, *, - name: Optional[str] = None, - email: Optional[str] = None, - dept_id: Optional[int] = None, - priority: Optional[str] = None, - source: Optional[str] = None, + name: str | None = None, + email: str | None = None, + dept_id: int | None = None, + priority: str | None = None, + source: str | None = None, ) -> dict: """ Create a ticket in osTicket. @@ -93,7 +93,7 @@ def create_ticket( } -def update_ticket(ticket_id: str, status: Optional[str] = None, note: Optional[str] = None) -> dict: +def update_ticket(ticket_id: str, status: str | None = None, note: str | None = None) -> dict: _require_config() payload = {} if status: @@ -131,7 +131,7 @@ def search_tickets(user_id: str, query: str, limit: int = 10) -> list[dict]: return out -def close_ticket(ticket_id: str, reason: Optional[str] = None) -> dict: +def close_ticket(ticket_id: str, reason: str | None = None) -> dict: _require_config() payload = {"status": "closed"} if reason: diff --git a/scripts/agent_server.py b/scripts/agent_server.py index 9178412..f6478ee 100644 --- a/scripts/agent_server.py +++ b/scripts/agent_server.py @@ -4,20 +4,16 @@ """ from __future__ import annotations -import asyncio -import json import logging import os import time -from typing import Optional import httpx import redis.asyncio as redis -from fastapi import FastAPI, HTTPException, Request, Depends +from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, Field - -from rate_limiter import RateLimiter, RateLimitConfig +from rate_limiter import RateLimitConfig, RateLimiter from session_manager import SessionManager # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• @@ -69,9 +65,9 @@ ) # Global clients -redis_client: Optional[redis.Redis] = None -rate_limiter: Optional[RateLimiter] = None -session_manager: Optional[SessionManager] = None +redis_client: redis.Redis | None = None +rate_limiter: RateLimiter | None = None +session_manager: SessionManager | None = None @app.on_event("startup") @@ -94,7 +90,7 @@ async def shutdown(): # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• class ChatRequest(BaseModel): - session_id: Optional[str] = None + session_id: str | None = None user_id: str message: str = Field(..., max_length=4000) platform: str = "web" diff --git a/scripts/analytics.py b/scripts/analytics.py index 3c569da..7425d2e 100644 --- a/scripts/analytics.py +++ b/scripts/analytics.py @@ -10,8 +10,7 @@ import json import os import sys -from datetime import datetime, timedelta -from typing import Optional +from datetime import datetime import asyncpg diff --git a/scripts/email_fetcher.py b/scripts/email_fetcher.py index 81d24e2..772458c 100644 --- a/scripts/email_fetcher.py +++ b/scripts/email_fetcher.py @@ -7,13 +7,11 @@ import email import imaplib -import json import logging import os import time import uuid from email.header import decode_header -from typing import Optional import httpx @@ -68,7 +66,7 @@ def extract_email_body(msg) -> str: return body[:10000] # Limit to 10KB -def process_email(mail: imaplib.IMAP4_SSL, num: str) -> Optional[dict]: +def process_email(mail: imaplib.IMAP4_SSL, num: str) -> dict | None: """Process a single email and create ticket.""" try: status, data = mail.fetch(num, "(RFC822)") diff --git a/scripts/health_monitor.py b/scripts/health_monitor.py index aa55266..a2d751f 100644 --- a/scripts/health_monitor.py +++ b/scripts/health_monitor.py @@ -10,7 +10,6 @@ import logging import os import time -from typing import Optional import httpx import redis.asyncio as redis @@ -31,7 +30,7 @@ } -async def check_service(name: str, url: Optional[str]) -> dict: +async def check_service(name: str, url: str | None) -> dict: """Check a single service health.""" if not url: return {"name": name, "status": "unknown", "latency_ms": 0} diff --git a/scripts/index_kb.py b/scripts/index_kb.py index 2654881..81ac0f9 100644 --- a/scripts/index_kb.py +++ b/scripts/index_kb.py @@ -6,16 +6,18 @@ from __future__ import annotations import hashlib -import json +import logging import os import sys from pathlib import Path +logger = logging.getLogger("index-kb") + try: import chromadb from chromadb.utils import embedding_functions except ImportError: - print("ERROR: chromadb not installed. Run: pip install chromadb") + logger.error("chromadb not installed. Run: pip install chromadb") sys.exit(1) CHROMA_URL = os.getenv("CHROMA_URL", "http://chroma:8000") @@ -39,11 +41,12 @@ def compute_hash(content: str) -> str: def main(): + logging.basicConfig(level=logging.INFO, format="%(message)s") client = chromadb.HttpClient(url=CHROMA_URL) # Get or create collection embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction( - model_name="all-MiniLM-L6-v2" + model_name="all-MiniLM-L6-v2", ) try: @@ -60,11 +63,11 @@ def main(): kb_path = Path(KB_DIR) if not kb_path.exists(): - print(f"ERROR: Knowledge base directory not found: {KB_DIR}") + logger.error("Knowledge base directory not found: %s", KB_DIR) sys.exit(1) files = list(kb_path.glob("*.md")) + list(kb_path.glob("*.txt")) - print(f"Found {len(files)} knowledge base files") + logger.info("Found %d knowledge base files", len(files)) total_chunks = 0 for file_path in files: @@ -81,7 +84,7 @@ def main(): # Check if content changed for meta in existing.get("metadatas", []): if meta and meta.get("hash") == content_hash: - print(f" [skip] {file_path.name} (unchanged)") + logger.info(" [skip] %s (unchanged)", file_path.name) break else: # Content changed, re-index @@ -111,11 +114,11 @@ def main(): metadatas=metadatas, ) total_chunks += len(chunks) - print(f" [indexed] {file_path.name}: {len(chunks)} chunks") + logger.info(" [indexed] %s: %d chunks", file_path.name, len(chunks)) # Get final count count = collection.count() - print(f"\nDone! Indexed {total_chunks} new chunks. Total in collection: {count}") + logger.info("\nDone! Indexed %d new chunks. Total in collection: %d", total_chunks, count) if __name__ == "__main__": diff --git a/scripts/rate_limiter.py b/scripts/rate_limiter.py index 8948d68..4525186 100644 --- a/scripts/rate_limiter.py +++ b/scripts/rate_limiter.py @@ -4,9 +4,8 @@ """ from __future__ import annotations -import time import logging -from typing import Optional +import time from dataclasses import dataclass, field logger = logging.getLogger(__name__) @@ -101,7 +100,7 @@ def check_request(self, session_id: str, user_id: str, message_length: int = 0) "session_remaining": int(self.config.max_session_duration - session_age), } - def get_session_info(self, session_id: str) -> Optional[dict]: + def get_session_info(self, session_id: str) -> dict | None: """Get current session info.""" state = self._sessions.get(session_id) if not state: diff --git a/scripts/session_manager.py b/scripts/session_manager.py index c3fbb5a..4a9dfbe 100644 --- a/scripts/session_manager.py +++ b/scripts/session_manager.py @@ -7,8 +7,6 @@ import json import logging import time -from typing import Optional -from dataclasses import dataclass, asdict logger = logging.getLogger(__name__) @@ -49,12 +47,12 @@ async def create_session(self, user_id: str, platform: str = "web", ip: str = No await conn.execute( """INSERT INTO sessions (id, user_id, platform, message_count, started_at, expires_at, active, ip_address) VALUES ($1, $2, $3, $4, NOW(), NOW() + INTERVAL '{} seconds', TRUE, $5::inet)""".format(self.max_duration), - session_id, user_id, platform, 0, ip + session_id, user_id, platform, 0, ip, ) return session_data - async def get_session(self, session_id: str) -> Optional[dict]: + async def get_session(self, session_id: str) -> dict | None: """Get session data.""" key = f"session:{session_id}" data = await self.redis.get(key) @@ -86,7 +84,7 @@ async def end_session(self, session_id: str): async with self.pg_pool.acquire() as conn: await conn.execute( "UPDATE sessions SET active = FALSE WHERE id = $1", - session_id + session_id, ) async def get_active_count(self) -> int: diff --git a/scripts/whatsapp_webhook.py b/scripts/whatsapp_webhook.py index e346647..261dfdf 100644 --- a/scripts/whatsapp_webhook.py +++ b/scripts/whatsapp_webhook.py @@ -12,11 +12,10 @@ import logging import os import time -from typing import Optional import httpx import redis.asyncio as redis -from fastapi import FastAPI, Request, HTTPException, Header +from fastapi import FastAPI, HTTPException, Request from pydantic import BaseModel, Field logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s") @@ -42,29 +41,29 @@ "check_tickets": [ "my ticket", "my tickets", "ticket status", "check ticket", "track ticket", "where is my ticket", "ticket #", "status of", "view tickets", "see tickets", - "list tickets", "my issue", "my problem", "ticket progress" + "list tickets", "my issue", "my problem", "ticket progress", ], "talk_to_human": [ "human", "agent", "person", "real person", "talk to someone", "speak to", "representative", "operator", "escalate", "manager", "don't want bot", "not helpful", "live chat", "live agent", "wait for someone", "can I talk", "I want help", "help please", - "I need help", "urgent", "asap", "emergency" + "I need help", "urgent", "asap", "emergency", ], "create_ticket": [ "create ticket", "new ticket", "open ticket", "report issue", "file complaint", "submit issue", "new problem", "new issue", "start ticket", "help with", "problem with", "issue with", - "something broken", "not working", "broken", "bug", "error" + "something broken", "not working", "broken", "bug", "error", ], "greeting": [ "hi", "hello", "hey", "good morning", "good afternoon", - "good evening", "howdy", "yo", "sup" + "good evening", "howdy", "yo", "sup", ], "thanks": [ "thanks", "thank you", "thx", "appreciate", "great", "awesome", - "perfect", "ok", "okay", "cool" - ] + "perfect", "ok", "okay", "cool", + ], } # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• @@ -152,7 +151,7 @@ "2๏ธโƒฃ Create a new ticket\n" "3๏ธโƒฃ Talk to a human\n\n" "Or just describe your issue and I'll do my best to help!" - ) + ), } # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• @@ -160,7 +159,7 @@ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• app = FastAPI(title="WhatsApp Helpdesk Webhook", version="1.0.0") -redis_client: Optional[redis.Redis] = None +redis_client: redis.Redis | None = None @app.on_event("startup") @@ -203,7 +202,7 @@ def detect_intent(message: str, session_data: dict) -> str: if session_data.get("awaiting") == "email": # Try to extract email from message import re - email_match = re.search(r'[\w.+-]+@[\w-]+\.[\w.-]+', text) + email_match = re.search(r"[\w.+-]+@[\w-]+\.[\w.-]+", text) if email_match: return "email_provided" return "ask_email" @@ -329,9 +328,9 @@ async def send_whatsapp_buttons(phone_number: str, body: str, buttons: list): "buttons": [ {"type": "reply", "reply": {"id": btn[0], "title": btn[1]}} for btn in buttons - ] - } - } + ], + }, + }, } async with httpx.AsyncClient(timeout=10) as client: @@ -361,7 +360,7 @@ async def add_to_human_queue(phone_number: str, session: dict): f"๐Ÿ”” *Human Support Request*\n\n" f"๐Ÿ“ฑ Customer: {phone_number}\n" f"โฐ Waiting since: Just now\n\n" - f"Reply to take over this conversation." + f"Reply to take over this conversation.", ) logger.info(f"Added {phone_number} to human queue, notified admin") @@ -403,7 +402,7 @@ async def receive_message(request: Request): if signature and WHATSAPP_WEBHOOK_SECRET != "change_me": body = await request.body() expected = "sha256=" + hmac.new( - WHATSAPP_WEBHOOK_SECRET.encode(), body, hashlib.sha256 + WHATSAPP_WEBHOOK_SECRET.encode(), body, hashlib.sha256, ).hexdigest() if not hmac.compare_digest(expected, signature): raise HTTPException(status_code=403, detail="Invalid signature") @@ -479,7 +478,7 @@ async def route_intent(intent: str, phone: str, text: str, session: dict) -> str if intent == "email_provided": import re - email_match = re.search(r'[\w.+-]+@[\w-]+\.[\w.-]+', text) + email_match = re.search(r"[\w.+-]+@[\w-]+\.[\w.-]+", text) if email_match: email = email_match.group() session["user_id"] = email @@ -498,7 +497,7 @@ async def route_intent(intent: str, phone: str, text: str, session: dict) -> str result = await forward_to_agent( user_id=session.get("user_id", phone), message=text, - platform="whatsapp" + platform="whatsapp", ) if result.get("ticket_id"): return RESPONSES["ticket_created"].format(**result) @@ -518,7 +517,7 @@ async def route_intent(intent: str, phone: str, text: str, session: dict) -> str result = await forward_to_agent( user_id=session.get("user_id", phone), message=text, - platform="whatsapp" + platform="whatsapp", ) return result.get("response", RESPONSES["fallback"]) @@ -532,8 +531,8 @@ async def search_tickets(email: str) -> list: json={ "user_id": email, "message": f"Search my tickets for: {email}", - "platform": "whatsapp" - } + "platform": "whatsapp", + }, ) if resp.status_code == 200: data = resp.json() @@ -553,8 +552,8 @@ async def forward_to_agent(user_id: str, message: str, platform: str) -> dict: json={ "user_id": user_id, "message": message, - "platform": platform - } + "platform": platform, + }, ) if resp.status_code == 200: return resp.json() @@ -635,6 +634,6 @@ async def view_queue(): queue.append({ "phone": data["phone"], "waiting_minutes": wait_minutes, - "message": data.get("message", "")[:100] + "message": data.get("message", "")[:100], }) return {"queue": queue, "total": len(queue)} diff --git a/ticket_platforms/__init__.py b/ticket_platforms/__init__.py index a8dc607..3d6d3b1 100644 --- a/ticket_platforms/__init__.py +++ b/ticket_platforms/__init__.py @@ -5,9 +5,7 @@ from ticket_platforms.registry import register, get, available """ -from . import osticket -from . import zammad -from . import email -from .registry import register, get, available +from . import email, osticket, zammad +from .registry import available, get, register __all__ = ["register", "get", "available", "osticket", "zammad", "email"] diff --git a/ticket_platforms/base.py b/ticket_platforms/base.py index 1b3df2c..1ad7ac2 100644 --- a/ticket_platforms/base.py +++ b/ticket_platforms/base.py @@ -6,7 +6,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import Any, Dict, List, Optional +from typing import Any class Ticket(ABC): @@ -17,23 +17,23 @@ def create_ticket( subject: str, body: str, *, - name: Optional[str] = None, - email: Optional[str] = None, - dept_id: Optional[int] = None, - priority: Optional[str] = None, - source: Optional[str] = None, + name: str | None = None, + email: str | None = None, + dept_id: int | None = None, + priority: str | None = None, + source: str | None = None, **kwargs: Any, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: raise NotImplementedError @abstractmethod def update_ticket( self, ticket_id: str, - status: Optional[str] = None, - note: Optional[str] = None, + status: str | None = None, + note: str | None = None, **kwargs: Any, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: raise NotImplementedError @abstractmethod @@ -43,14 +43,14 @@ def search_tickets( query: str, limit: int = 10, **kwargs: Any, - ) -> List[Dict[str, Any]]: + ) -> list[dict[str, Any]]: raise NotImplementedError @abstractmethod def close_ticket( self, ticket_id: str, - reason: Optional[str] = None, + reason: str | None = None, **kwargs: Any, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: raise NotImplementedError diff --git a/ticket_platforms/email.py b/ticket_platforms/email.py index 28fb03d..93523bc 100644 --- a/ticket_platforms/email.py +++ b/ticket_platforms/email.py @@ -4,14 +4,7 @@ from __future__ import annotations -import email as imap_email -import email.header -import imaplib -import re -from typing import Any, Dict, List, Optional - from .base import Ticket -from .registry import get as get_platform class EmailTicketAdapter: @@ -28,4 +21,4 @@ def __init__(self, *, imap_host: str, imap_port: int, username: str, password: s self.mailbox = mailbox self.mark_seen = mark_seen self.ticket_platform_name = ticket_platform - self._ticket_platform: Optional[Ticket] = None + self._ticket_platform: Ticket | None = None diff --git a/ticket_platforms/freshdesk.py b/ticket_platforms/freshdesk.py index fd0bf33..b1ff8f9 100644 --- a/ticket_platforms/freshdesk.py +++ b/ticket_platforms/freshdesk.py @@ -6,11 +6,10 @@ import base64 import json -from typing import Any, Dict, List, Optional - -import urllib.request import urllib.error import urllib.parse +import urllib.request +from typing import Any from .base import Ticket from .registry import register @@ -24,7 +23,7 @@ class FreshdeskAdapter(Ticket): API key is base64 encoded as 'API_KEY:X' for Basic Auth. """ - def __init__(self, *, base_url: str, api_key: str, domain: Optional[str] = None): + def __init__(self, *, base_url: str, api_key: str, domain: str | None = None): """ Args: base_url: Freshdesk URL, e.g. https://yourcompany.freshdesk.com @@ -43,7 +42,7 @@ def _get_auth_header(self) -> str: token = base64.b64encode(f"{self.api_key}:X".encode()).decode() return f"Basic {token}" - def _request(self, path: str, *, method: str = "GET", data: Optional[Dict] = None) -> Dict[str, Any]: + def _request(self, path: str, *, method: str = "GET", data: dict | None = None) -> dict[str, Any]: """Make an authenticated request to Freshdesk API.""" url = f"{self.api_url}{path}" headers = { @@ -65,15 +64,15 @@ def create_ticket( subject: str, body: str, *, - name: Optional[str] = None, - email: Optional[str] = None, - dept_id: Optional[int] = None, - priority: Optional[str] = None, - source: Optional[str] = None, + name: str | None = None, + email: str | None = None, + dept_id: int | None = None, + priority: str | None = None, + source: str | None = None, **kwargs: Any, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Create a new ticket in Freshdesk.""" - data: Dict[str, Any] = { + data: dict[str, Any] = { "subject": subject, "description": body, "email": email or user_id, @@ -98,12 +97,12 @@ def create_ticket( def update_ticket( self, ticket_id: str, - status: Optional[str] = None, - note: Optional[str] = None, + status: str | None = None, + note: str | None = None, **kwargs: Any, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Update ticket: add note, change status.""" - data: Dict[str, Any] = {} + data: dict[str, Any] = {} # Map status: open=2, pending=3, resolved=4, closed=5 status_map = {"open": 2, "pending": 3, "resolved": 4, "closed": 5} if status: @@ -126,7 +125,7 @@ def search_tickets( query: str, limit: int = 10, **kwargs: Any, - ) -> List[Dict[str, Any]]: + ) -> list[dict[str, Any]]: """Search tickets by user email.""" # Freshdesk search API search_query = f"email:'{user_id}'" @@ -152,11 +151,11 @@ def search_tickets( def close_ticket( self, ticket_id: str, - reason: Optional[str] = None, + reason: str | None = None, **kwargs: Any, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Close a ticket (status=5).""" - data: Dict[str, Any] = {"status": 5} + data: dict[str, Any] = {"status": 5} if reason: self._request(f"/tickets/{ticket_id}/notes", method="POST", data={ "body": f"Closed: {reason}", diff --git a/ticket_platforms/osticket.py b/ticket_platforms/osticket.py index 90184aa..c2aee62 100644 --- a/ticket_platforms/osticket.py +++ b/ticket_platforms/osticket.py @@ -4,10 +4,10 @@ from __future__ import annotations -import os import re +from typing import Any + import requests -from typing import Any, Dict, List, Optional from .base import Ticket from .registry import register @@ -61,7 +61,7 @@ def create_ticket(self, user_id, subject, body, *, name=None, email=None, dept_i return {"ticket_id": str(ticket.get("ticket_id") or data.get("id", "")), "number": ticket.get("number"), "status": ticket.get("status"), "subject": subject, "platform": "osticket"} def update_ticket(self, ticket_id, status=None, note=None, **kwargs): - payload: Dict[str, Any] = {} + payload: dict[str, Any] = {} if status: payload["status"] = status if note: @@ -80,7 +80,7 @@ def search_tickets(self, user_id, query, limit=10, **kwargs): return [{"ticket_id": str(t.get("ticket_id") or t.get("id")), "number": t.get("number"), "subject": t.get("subject"), "status": t.get("status")} for t in tickets] def close_ticket(self, ticket_id, reason=None, **kwargs): - payload: Dict[str, Any] = {"status": "closed"} + payload: dict[str, Any] = {"status": "closed"} if reason: payload["post"] = self._sanitize(reason) payload["post_status"] = "closed" diff --git a/ticket_platforms/registry.py b/ticket_platforms/registry.py index 1dd2bbc..ecb1586 100644 --- a/ticket_platforms/registry.py +++ b/ticket_platforms/registry.py @@ -5,23 +5,20 @@ from __future__ import annotations -from typing import Dict, Type - from .base import Ticket - -_REGISTRY: Dict[str, Type["Ticket"]] = {} +_REGISTRY: dict[str, type["Ticket"]] = {} def register(name: str): - def decorator(cls: Type[Ticket]): + def decorator(cls: type[Ticket]): _REGISTRY[name.lower()] = cls return cls return decorator -def get(name: str) -> Type[Ticket]: +def get(name: str) -> type[Ticket]: key = name.lower() if key not in _REGISTRY: raise KeyError(f"Unknown ticket platform: {name}. Registered: {sorted(_REGISTRY)}") diff --git a/ticket_platforms/zammad.py b/ticket_platforms/zammad.py index b49433c..d680c72 100644 --- a/ticket_platforms/zammad.py +++ b/ticket_platforms/zammad.py @@ -4,8 +4,7 @@ from __future__ import annotations -import os -from typing import Any, Dict, List, Optional +from typing import Any from .base import Ticket from .registry import register @@ -13,7 +12,7 @@ @register("zammad") class ZammadAdapter(Ticket): - def __init__(self, *, base_url: str, api_token: str, group_id: Optional[int] = None, priority_id: int = 2, state_id: int = 1): + def __init__(self, *, base_url: str, api_token: str, group_id: int | None = None, priority_id: int = 2, state_id: int = 1): self.base_url = base_url.rstrip("/") self.api_token = api_token self.group_id = group_id @@ -44,7 +43,7 @@ def create_ticket(self, user_id, subject, body, *, name=None, email=None, priori return {"ticket_id": str(data.get("id")), "number": data.get("number"), "status": data.get("state", {}).get("name"), "subject": subject, "platform": "zammad"} def update_ticket(self, ticket_id, status=None, note=None, **kwargs): - payload: Dict[str, Any] = {} + payload: dict[str, Any] = {} if status: payload["state"] = status if note: @@ -67,7 +66,7 @@ def search_tickets(self, user_id, query, limit=10, **kwargs): return [{"ticket_id": str(t.get("id")), "number": t.get("number"), "subject": t.get("title"), "status": (t.get("state") or {}).get("name") if isinstance(t.get("state"), dict) else t.get("state")} for t in tickets] def close_ticket(self, ticket_id, reason=None, **kwargs): - payload: Dict[str, Any] = {"state": "closed"} + payload: dict[str, Any] = {"state": "closed"} if reason: payload["article"] = {"type": "text", "body": reason, "internal": True} r = requests.patch(self._url(f"tickets/{ticket_id}"), json=payload, headers=self._headers(), timeout=30) From 6d09d36209a46b6e917e75af485caad3dd68a9f3 Mon Sep 17 00:00:00 2001 From: OneByJorah Date: Sun, 5 Jul 2026 22:49:34 -0400 Subject: [PATCH 6/6] fix(audit): resolve 6 critical bugs and add missing standard files C1: rate_limiter dict typo fix. C2: zammad import requests. C3: session_manager SQL injection fix. C4/C5: Redis URL fixes. C6: email_fetcher endpoint fix. Added .dockerignore, j1.yaml, CHANGELOG.md, CODEOWNERS. --- .dockerignore | 50 +++++++++++ .github/CODEOWNERS | 12 +++ AUDIT_REPORT.md | 162 ++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 28 +++++++ j1.yaml | 16 ++++ scripts/analytics.py | 2 +- scripts/email_fetcher.py | 14 ++-- scripts/health_monitor.py | 22 +++-- scripts/rate_limiter.py | 2 +- scripts/session_manager.py | 44 ++++++---- scripts/whatsapp_webhook.py | 5 +- ticket_platforms/zammad.py | 2 + 12 files changed, 320 insertions(+), 39 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/CODEOWNERS create mode 100644 AUDIT_REPORT.md create mode 100644 CHANGELOG.md create mode 100644 j1.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f0c8069 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,50 @@ +# Git +.git +.gitignore +.gitattributes + +# Secrets +.env +.env.* +.secrets/ +certs/ +*.pem +*.key + +# Models +models/ +*.gguf + +# Data +data/ +knowledge-base/ +email-queue/ + +# IDE +.idea/ +.vscode/ +*.swp + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log + +# CI +.github/ + +# Docker +Dockerfile* +docker-compose* + +# Node (for tools-ui) +node_modules/ + +# Documentation +README.md +CONTRIBUTING.md +SECURITY.md +CODE_OF_CONDUCT.md +AUDIT_REPORT.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..36d288e --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,12 @@ +# Default owners for all files +* @OneByJorah + +# Security-sensitive files +SECURITY.md @OneByJorah +config/ @OneByJorah +docker-compose*.yml @OneByJorah +Dockerfile* @OneByJorah + +# CI/CD +.github/workflows/ @OneByJorah +.github/dependabot.yml @OneByJorah diff --git a/AUDIT_REPORT.md b/AUDIT_REPORT.md new file mode 100644 index 0000000..5dbe0de --- /dev/null +++ b/AUDIT_REPORT.md @@ -0,0 +1,162 @@ +# AUDIT_REPORT โ€” CommandDesk + +**Date:** 2026-07-05 +**Auditor:** J1-PIPELINE (Phase 1 โ€” AUDITOR) +**Status:** `DEGRADED` (Score: 74/100) + +--- + +## 1. PROJECT CLASSIFICATION + +| Field | Value | +|---|---| +| **Repo** | CommandDesk | +| **Class** | AI / Helpdesk / Full-Stack | +| **Primary Language** | Python 3.11 | +| **Framework** | FastAPI | +| **Deployment** | Docker Compose | +| **Tailscale Only** | No (exposed ports: 80, 443, 8383) | + +--- + +## 2. README COMPLIANCE (README_STANDARD.md) + +| Requirement | Status | Notes | +|---|---|---| +| One-line positioning | โœ… | "Self-Hosted AI Helpdesk Agent" | +| Max 3 badges | โŒ | Has **5 badges** (Python, FastAPI, Docker, AI, MIT) | +| 60-second quick start | โœ… | `git clone` โ†’ `cp .env.example .env` โ†’ `docker compose up -d` | +| Features (3-5 bullets) | โœ… | 8 features listed | +| Architecture diagram | โœ… | ASCII diagram present | +| Contributing section | โœ… | Links to CONTRIBUTING.md | +| License section | โœ… | MIT | + +**Issue:** README has 5 badges โ€” exceeds the 3-badge max per README_STANDARD.md. + +--- + +## 3. CODE QUALITY โ€” CRITICAL & DEGRADED ITEMS + +### ๐Ÿ”ด CRITICAL (Must Fix) + +| # | File | Issue | Severity | +|---|---|---|---| +| C1 | `rate_limiter.py:64` | **Bug:** `self._sessions: dict[str, SessionState] =()` โ€” Empty tuple instead of dict `{}`. Causes `AttributeError` on first access. | CRITICAL | +| C2 | `zammad.py` | **Missing import:** Uses `requests.post()`, `requests.patch()`, `requests.get()` but `import requests` is missing. Will crash at runtime. | CRITICAL | +| C3 | `session_manager.py:38` | **SQL Injection:** `INTERVAL '{} seconds'".format(self.max_duration)` โ€” Uses string formatting for SQL query parameter. | CRITICAL | +| C4 | `health_monitor.py:22` | **Wrong Redis URL:** `redis://redis:***@postgres:5432/helpdesk` โ€” References PostgreSQL port (5432) instead of Redis port (6379), and uses `***` as password placeholder in a default. | CRITICAL | +| C5 | `whatsapp_webhook.py:29` | **Same wrong Redis URL:** `redis://redis:***@postgres:5432/helpdesk` | CRITICAL | +| C6 | `email_fetcher.py:83` | **Missing API endpoint:** POSTs to `/tickets/create` but no such route exists in `agent_server.py` โ€” only `/chat` and `/health` are defined. Feature is broken. | CRITICAL | + +### ๐ŸŸก DEGRADED (Should Fix) + +| # | File | Issue | Severity | +|---|---|---|---| +| D1 | `rate_limiter.py` | **In-memory only:** Redis client is stored but never used. Rate limiting won't work across replicas/restarts. | DEGRADED | +| D2 | `agent_server.py` | **Blocking calls in async:** `get_system_prompt()` (file I/O) and `rate_limiter.check_request()` are sync methods called from async endpoints. | DEGRADED | +| D3 | `session_manager.py:92` | **O(N) `redis.keys()`:** Used in `get_active_count()` and `cleanup_expired()` โ€” dangerous in production with many sessions. | DEGRADED | +| D4 | `health_monitor.py:83` | **O(N) `redis.keys()`:** Same pattern as D3. | DEGRADED | +| D5 | `analytics.py:10` | **Hardcoded password in URL:** `postgresql://helpdesk:***@postgres:5432/helpdesk` | DEGRADED | +| D6 | `whatsapp_webhook.py:138` | **Import inside function:** `import re` should be at module level. | DEGRADED | + +--- + +## 4. SECURITY AUDIT (GUARDIAN โ€” Phase 3 Preview) + +| Check | Status | Notes | +|---|---|---| +| Default credentials | โŒ | `change_me` passwords throughout: JWT_SECRET, CHROMA_AUTH_TOKEN, WHATSAPP_WEBHOOK_SECRET, DB_PASSWORD, REDIS_PASSWORD | +| HTTPS enforced | โŒ | Nginx listens on port 80 only (no 443 config). `docker-compose.prod.yml` exposes 443 but no TLS certs configured. | +| CORS | โš ๏ธ | `allow_origins=["*"]` โ€” acceptable for development, but not documented as such | +| Secrets in repo | โœ… | `.env` in `.gitignore`, `*.pem`, `*.key` excluded | +| Gitleaks clean | โœ… | No hardcoded secrets found in code | +| Rate limiting | โœ… | Nginx + app-level rate limiting implemented | +| Docker rootless | โš ๏ธ | No non-root user specified in Dockerfiles | +| Content-Security-Policy | โœ… | Set in nginx.conf | +| SBOM | โŒ | No SBOM (CycloneDX or SPDX) present | +| Security.md | โœ… | Present with contact + disclosure policy | +| Auth for admin | โš ๏ธ | `ADMIN_API_KEY` configured but no auth middleware seen in agent_server.py | + +--- + +## 5. DOCKER STANDARD CHECK + +| Check | Status | Notes | +|---|---|---| +| Health checks | โœ… | Defined for all services | +| Named volumes | โœ… | All volumes named | +| `.dockerignore` | โŒ | **Missing** โ€” `.env`, `.git`, `node_modules` could leak into build context | +| Multi-stage builds | โŒ | Dockerfile is single-stage | +| Image version tags | โš ๏ธ | `searxng:latest`, `n8n:latest` โ€” not pinned | +| Non-root user | โŒ | Root user used in all Dockerfiles | +| Tailscale pattern | โŒ | Not implemented | + +--- + +## 6. GITHUB STANDARD CHECK + +| Check | Status | Notes | +|---|---|---| +| Repo description | โœ… | Set | +| Topics | โš ๏ธ | Could add more specific topics | +| Branch protection | โš ๏ธ | Not verified (requires GH API) | +| CODEOWNERS | โŒ | **Missing** | +| Dependabot | โœ… | `.github/dependabot.yml` present | +| Issue templates | โœ… | Bug report + feature request | +| PR template | โœ… | Present | +| CI workflows | โœ… | CI + CodeQL workflows present | + +--- + +## 7. MISSING STANDARD FILES + +| File | Purpose | Status | +|---|---|---| +| `j1.yaml` | Pipeline registry metadata | โŒ Missing | +| `INTENT.md` | Engineering intent (Phase -1 ORACLE) | โŒ Missing | +| `.dockerignore` | Docker build context optimization | โŒ Missing | +| `CHANGELOG.md` | Release history | โŒ Missing | +| `CODEOWNERS` | PR ownership routing | โŒ Missing | +| `pyproject.toml` / `setup.py` | Python package metadata | โŒ Missing | + +--- + +## 8. PRODUCTION SCORE + +| Category | Weight | Score | Weighted | +|---|---|---|---| +| Security | 20% | 65 | 13.0 | +| Architecture | 15% | 75 | 11.25 | +| Documentation | 15% | 80 | 12.0 | +| Testing | 15% | 55 | 8.25 | +| Deployment | 10% | 78 | 7.8 | +| Automation | 10% | 82 | 8.2 | +| GitHub Quality | 10% | 85 | 8.5 | +| Branding | 5% | 85 | 4.25 | + +**Total Score: 73.25 / 100 โ€” `DEGRADED`** (threshold: 90) + +--- + +## 9. RECOMMENDED ACTIONS + +### Immediate (CRITICAL fixes): +1. Fix `self._sessions = {}` in `rate_limiter.py:64` (typo `()` โ†’ `{}`) +2. Add `import requests` to `zammad.py` +3. Fix SQL injection in `session_manager.py:38` โ€” use parameterized query +4. Fix Redis URLs in `health_monitor.py` and `whatsapp_webhook.py` (port 6379, proper placeholder) +5. Add `/tickets/create` endpoint to `agent_server.py` or fix `email_fetcher.py` to use `/chat` + +### Short-term: +6. Add `.dockerignore` +7. Pin Docker image versions (no `:latest`) +8. Fix blocking calls in async endpoints +9. Replace `redis.keys()` with `SCAN` in session_manager and health_monitor +10. Add `import re` at top of `whatsapp_webhook.py` + +### Medium-term: +11. Create `j1.yaml` and `INTENT.md` +12. Create `CHANGELOG.md` +13. Add CODEOWNERS file +14. Implement Redis-backed rate limiting (not just in-memory) +15. Add non-root user to Dockerfiles diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1e61859 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog + +All notable changes to CommandDesk will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Fixed + +- **CRITICAL:** Fixed `rate_limiter.py` dict initialization typo (`=()` โ†’ `={}`) that broke rate limiting entirely +- **CRITICAL:** Added missing `import requests` to `zammad.py` โ€” would crash on ticket operations +- **CRITICAL:** Fixed SQL injection vulnerability in `session_manager.py` โ€” replaced string formatting with parameterized query +- **CRITICAL:** Fixed wrong Redis URLs in `health_monitor.py` and `whatsapp_webhook.py` (referenced PostgreSQL port 5432 instead of Redis port 6379) +- **CRITICAL:** Fixed `email_fetcher.py` to route email-to-ticket through existing `/chat` endpoint instead of non-existent `/tickets/create` +- Moved `import re` to module level in `whatsapp_webhook.py` (was inside function) +- Removed hardcoded placeholder password from `analytics.py` default URL +- Replaced `redis.keys()` with `SCAN` cursor iteration in `session_manager.py` and `health_monitor.py` to avoid O(N) blocking calls +- Added `.dockerignore` to prevent build context leaks +- Added `j1.yaml` for pipeline registry metadata + +### Changed + +- `session_manager.py`: `import uuid` moved to module level +- `health_monitor.py`: Fixed Redis URL default to proper format +- `whatsapp_webhook.py`: Fixed Redis URL default to proper format +- `analytics.py`: PostgreSQL URL default now empty (must be set via env) diff --git a/j1.yaml b/j1.yaml new file mode 100644 index 0000000..b639405 --- /dev/null +++ b/j1.yaml @@ -0,0 +1,16 @@ +repo: CommandDesk +class: AI / Helpdesk +org: OneByJorah +owner: Jhonattan L. Jimenez +license: MIT +production_score: 73 +last_audit: "2026-07-05" +last_publish: null +standards_version: "2.1" +dependencies: + - JorahOne +deploy_target: scratch +tailscale_only: false +public_facing: false +community_sla_hours: 48 +adoption_tracked: false diff --git a/scripts/analytics.py b/scripts/analytics.py index 7425d2e..406000d 100644 --- a/scripts/analytics.py +++ b/scripts/analytics.py @@ -14,7 +14,7 @@ import asyncpg -POSTGRES_URL = os.getenv("POSTGRES_URL", "postgresql://helpdesk:***@postgres:5432/helpdesk") +POSTGRES_URL = os.getenv("POSTGRES_URL", "") # Must be set via environment variable async def get_token_usage(pool, hours: int = 24) -> dict: diff --git a/scripts/email_fetcher.py b/scripts/email_fetcher.py index 772458c..90a82b0 100644 --- a/scripts/email_fetcher.py +++ b/scripts/email_fetcher.py @@ -86,20 +86,20 @@ def process_email(mail: imaplib.IMAP4_SSL, num: str) -> dict | None: logger.info(f"Processing email: {subject[:50]} from {email_addr}") - # Create ticket via helpdesk agent API + # Create ticket via helpdesk agent /chat endpoint ticket_data = { - "subject": subject, - "body": body, - "user_email": email_addr, + "user_id": email_addr, + "message": f"New ticket from {email_addr}: {subject}\ +\ +{body}", "platform": TICKET_PLATFORM, - "message_id": message_id, } with httpx.Client(timeout=30) as client: - resp = client.post(f"{HELPDESK_AGENT_URL}/tickets/create", json=ticket_data) + resp = client.post(f"{HELPDESK_AGENT_URL}/chat", json=ticket_data) if resp.status_code == 200: result = resp.json() - logger.info(f"Ticket created: {result.get('ticket_id')}") + logger.info(f"Email processed: {subject[:50]} -> session {result.get('session_id', 'N/A')}") return result else: logger.warning(f"Ticket creation failed: {resp.status_code} {resp.text[:200]}") diff --git a/scripts/health_monitor.py b/scripts/health_monitor.py index a2d751f..8fbd540 100644 --- a/scripts/health_monitor.py +++ b/scripts/health_monitor.py @@ -17,7 +17,7 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s") logger = logging.getLogger("health-monitor") -REDIS_URL = os.getenv("REDIS_URL", "redis://redis:***@postgres:5432/helpdesk") +REDIS_URL = os.getenv("REDIS_URL", "redis://:redis_pass@redis:6379/0") SERVICES = { "llama": "http://llama:8081/health", @@ -74,16 +74,20 @@ async def collect_metrics(redis_client: redis.Redis) -> dict: redis_memory = "unknown" redis_keys = 0 - # Active sessions + # Active sessions (using SCAN to avoid O(N) KEYS) try: - session_keys = await redis_client.keys("session:*") active_sessions = 0 - for key in session_keys: - data = await redis_client.get(key) - if data: - session = json.loads(data) - if session.get("active"): - active_sessions += 1 + cursor = 0 + while True: + cursor, session_keys = await redis_client.scan(cursor, match="session:*", count=1000) + for key in session_keys: + data = await redis_client.get(key) + if data: + session = json.loads(data) + if session.get("active"): + active_sessions += 1 + if cursor == 0: + break except Exception: active_sessions = 0 diff --git a/scripts/rate_limiter.py b/scripts/rate_limiter.py index 4525186..b2666ac 100644 --- a/scripts/rate_limiter.py +++ b/scripts/rate_limiter.py @@ -36,7 +36,7 @@ class RateLimiter: def __init__(self, config: RateLimitConfig, redis_client=None): self.config = config self.redis = redis_client - self._sessions: dict[str, SessionState] =() + self._sessions: dict[str, SessionState] = {} def check_request(self, session_id: str, user_id: str, message_length: int = 0) -> dict: """ diff --git a/scripts/session_manager.py b/scripts/session_manager.py index 4a9dfbe..8caef11 100644 --- a/scripts/session_manager.py +++ b/scripts/session_manager.py @@ -7,6 +7,7 @@ import json import logging import time +import uuid logger = logging.getLogger(__name__) @@ -21,7 +22,6 @@ def __init__(self, redis_client, postgres_pool=None, max_duration: int = 7200): async def create_session(self, user_id: str, platform: str = "web", ip: str = None) -> dict: """Create a new session.""" - import uuid session_id = str(uuid.uuid4()) now = time.time() expires_at = now + self.max_duration @@ -46,8 +46,8 @@ async def create_session(self, user_id: str, platform: str = "web", ip: str = No async with self.pg_pool.acquire() as conn: await conn.execute( """INSERT INTO sessions (id, user_id, platform, message_count, started_at, expires_at, active, ip_address) - VALUES ($1, $2, $3, $4, NOW(), NOW() + INTERVAL '{} seconds', TRUE, $5::inet)""".format(self.max_duration), - session_id, user_id, platform, 0, ip, + VALUES ($1, $2, $3, $4, NOW(), NOW() + MAKE_INTERVAL(secs => $5), TRUE, $6::inet)""", + session_id, user_id, platform, 0, self.max_duration, ip, ) return session_data @@ -88,24 +88,32 @@ async def end_session(self, session_id: str): ) async def get_active_count(self) -> int: - """Get count of active sessions.""" - keys = await self.redis.keys("session:*") + """Get count of active sessions using SCAN (avoid O(N) KEYS).""" count = 0 - for key in keys: - data = await self.redis.get(key) - if data: - session = json.loads(data) - if session.get("active"): - count += 1 + cursor = 0 + while True: + cursor, keys = await self.redis.scan(cursor, match="session:*", count=1000) + for key in keys: + data = await self.redis.get(key) + if data: + session = json.loads(data) + if session.get("active"): + count += 1 + if cursor == 0: + break return count async def cleanup_expired(self): - """Remove expired sessions.""" - keys = await self.redis.keys("session:*") + """Remove expired sessions using SCAN (avoid O(N) KEYS).""" removed = 0 - for key in keys: - ttl = await self.redis.ttl(key) - if ttl < 0: - await self.redis.delete(key) - removed += 1 + cursor = 0 + while True: + cursor, keys = await self.redis.scan(cursor, match="session:*", count=1000) + for key in keys: + ttl = await self.redis.ttl(key) + if ttl < 0: + await self.redis.delete(key) + removed += 1 + if cursor == 0: + break return removed diff --git a/scripts/whatsapp_webhook.py b/scripts/whatsapp_webhook.py index 261dfdf..8b8364e 100644 --- a/scripts/whatsapp_webhook.py +++ b/scripts/whatsapp_webhook.py @@ -11,6 +11,7 @@ import json import logging import os +import re import time import httpx @@ -26,7 +27,7 @@ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• HELPDESK_AGENT_URL = os.getenv("HELPDESK_AGENT_URL", "http://helpdesk-agent:8080") -REDIS_URL = os.getenv("REDIS_URL", "redis://redis:***@postgres:5432/helpdesk") +REDIS_URL = os.getenv("REDIS_URL", "redis://:redis_pass@redis:6379/0") WHATSAPP_WEBHOOK_SECRET = os.getenv("WHATSAPP_WEBHOOK_SECRET", "change_me") WHATSAPP_TOKEN = os.getenv("WHATSAPP_TOKEN", "") WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID", "") @@ -201,7 +202,6 @@ def detect_intent(message: str, session_data: dict) -> str: # Check if we're waiting for a specific response if session_data.get("awaiting") == "email": # Try to extract email from message - import re email_match = re.search(r"[\w.+-]+@[\w-]+\.[\w.-]+", text) if email_match: return "email_provided" @@ -477,7 +477,6 @@ async def route_intent(intent: str, phone: str, text: str, session: dict) -> str return RESPONSES["human_queue"] if intent == "email_provided": - import re email_match = re.search(r"[\w.+-]+@[\w-]+\.[\w.-]+", text) if email_match: email = email_match.group() diff --git a/ticket_platforms/zammad.py b/ticket_platforms/zammad.py index d680c72..1d1ee8e 100644 --- a/ticket_platforms/zammad.py +++ b/ticket_platforms/zammad.py @@ -6,6 +6,8 @@ from typing import Any +import requests + from .base import Ticket from .registry import register