Skip to main content
Glama
README.md
# πŸ€– ARIA β€” Internal Knowledge & Ticketing Assistant

> **MCP-connected chatbot for IT operations teams β€” RAG answers from Confluence, writes new articles when nothing exists, and turns chat into clean Jira tickets, streamed live**

ARIA sits in front of six internal teams β€” **Service Desk, Command Center, Network, Linux, Database, and Windows Engineering** β€” as one chat interface. A LangChain probing layer reads each question first: if it's too vague to search on, ARIA asks one clarifying question instead of guessing. Once the issue is clear, a ChromaDB-backed retrieval loop finds the best matching Confluence article β€” rewording and re-searching if the first pass isn't confident β€” and streams a grounded answer token-by-token. If nothing matches, ARIA drafts and publishes a new article, so the knowledge base grows itself. A **"Log as Ticket"** action turns the raw back-and-forth of the chat into a well-formed Jira Service Desk ticket, not a dump of the conversation.

![Python](https://img.shields.io/badge/Python-3.10+-3776AB?style=flat&logo=python&logoColor=white)
![FastAPI](https://img.shields.io/badge/FastAPI-0.112+-009688?style=flat&logo=fastapi&logoColor=white)
![LangChain](https://img.shields.io/badge/LangChain-probing_layer-1C3C3C?style=flat)
![ChromaDB](https://img.shields.io/badge/ChromaDB-RAG-6366f1?style=flat)
![MCP](https://img.shields.io/badge/MCP-Server-8b5cf6?style=flat)
![License](https://img.shields.io/badge/License-MIT-green?style=flat)

---

## ✨ Features

| Feature | Description |
|---|---|
| πŸ—‚οΈ **Six team spaces** | Service Desk Β· Command Center Β· Network Β· Linux Β· Database Β· Windows Engineering, each with its own Confluence space + Jira project |
| 🧭 **LangChain probing layer** | Reads the question before anything else runs; a genuinely vague report ("my thing is broken") gets one clarifying question instead of a guess |
| πŸ”Ž **RAG retrieval (ChromaDB)** | Articles are chunked and embedded, not dumped whole into the prompt; retrieval is confidence-scored and re-tried with a reworded query when the first pass is weak |
| ✍️ **Self-healing KB** | No confident match β†’ ARIA drafts an article, publishes it, and indexes it immediately β€” the next person with the same question finds it already there |
| 🌊 **Real-time streaming** | Answers stream token-by-token over SSE with a "Musing…" / "Reading…" / "Drafting…" status and a blinking cursor, like a live typed reply |
| 🌑️ **temperature=0.2 everywhere** | Every completion call β€” decision, synthesis, drafting, ticket writing β€” is grounded and low-temperature on purpose; prompts explicitly forbid inventing steps not in the source |
| πŸ›‘οΈ **Guardrails** | Hard-blocks high-sensitivity PHI/PII (SSN, card numbers, MRNs), sexual/nudity content, and security-risk requests (credential theft, bypassing auth) *before* anything reaches an LLM, Confluence, or Jira |
| πŸ”“ **Soft PII redaction** | Low-sensitivity contact info (email, phone, IP) is masked but still passed through β€” a ticket needs a reporter's email |
| 🎫 **Log as Ticket** | Converts the chat's raw comments into a clean summary + description + priority and files it in Jira Service Desk |
| πŸ”Œ **MCP server** | Every capability is also exposed as an MCP tool for Claude Desktop / Claude Code |
| πŸ§ͺ **Offline-first** | Local JSON knowledge base + ticket store, fully demo-able with no Jira/Confluence/Okta tenant; flip one flag to go live |

---

## πŸ—οΈ Project Structure

```
aria-service-desk-assistant/
β”œβ”€β”€ main.py                 ← FastAPI app: /api/chat, /api/chat/stream (SSE), /api/ticket, /api/teams
β”œβ”€β”€ mcp_server.py            ← Same capabilities exposed over MCP (stdio)
β”œβ”€β”€ config.py                ← Settings: mock mode, temperature, Jira/Confluence/Okta creds
β”œβ”€β”€ models.py                 ← Team enum + Pydantic request/response models
β”œβ”€β”€ prompts.py                ← Prompt templates (decision, synthesis, drafting, ticket writer)
β”œβ”€β”€ probe_agent.py             ← LangChain layer: clarity check + search-intent reword loop
β”œβ”€β”€ vector_store.py              ← ChromaDB chunking/embedding/retrieval
β”œβ”€β”€ knowledge_agent.py            ← Orchestrates probe β†’ retrieve β†’ synthesize/draft, streamed
β”œβ”€β”€ ticket_writer.py                ← Chat transcript β†’ clean ticket summary/description
β”œβ”€β”€ guardrails.py                    ← Hard blocks: PHI/PII, sexual content, security risk
β”œβ”€β”€ phi_filter.py                     ← Soft redaction: email/phone/IP, masked not blocked
β”œβ”€β”€ confluence_client.py               ← Search + publish articles (mock JSON ↔ real Confluence REST)
β”œβ”€β”€ jira_client.py                      ← Create tickets (mock JSON ↔ real Jira REST)
β”œβ”€β”€ okta_client.py                       ← Requester lookup, name/department only (mock ↔ real Okta REST)
β”œβ”€β”€ kb_store.py                           ← Local JSON-backed knowledge base (the mock/offline Confluence)
β”œβ”€β”€ data/
β”‚   β”œβ”€β”€ mock_kb.json                       ← Seed articles per team (Troubleshooting/How-To/Runbook)
β”‚   β”œβ”€β”€ chroma/ (runtime)                   ← Local vector index
β”‚   └── mock_tickets.json (runtime)          ← Filed tickets in mock mode
└── static/
    └── index.html                           ← Chat UI: musing status, streaming text, blinking cursor
```

---

## πŸ”„ Chat Flow

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ USER picks a team, asks a question                                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                  β–Ό
                POST /api/chat/stream  β†’  guardrails.screen()  β†’ blocked? refuse & stop
                                  β”‚
                                  β–Ό
               probe_agent.assess()  (LangChain: ChatOpenAI + prompt template)
                                  β”‚
                     clear enough to search?
                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
               NO                                   YES
                β”‚                                     β”‚
   stream back ONE clarifying              vector_store: chunk + embed (ChromaDB)
   question, wait for the reply                        β”‚
                                            best_articles_context(team, intent)
                                                         β”‚
                                          LLM: found? confidence 0.0–1.0
                                        low confidence β†’ probe_agent.reword()
                                          β†’ re-embed search β†’ retry (max 2 rounds)
                                                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”
                                            confident match     still nothing
                                                β”‚                     β”‚
                                  stream synthesis from        stream a drafted
                                  the one matched article       article, publish +
                                  (source: knowledge_base)       index it immediately
                                                β”‚                (source: drafted_article)
                                                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                            β–Ό
                                              ChatResponse events β†’ SSE β†’
                                              UI renders with blinking cursor
```

## 🎫 Log as Ticket Flow

```
USER clicks "Log as Ticket" on the current conversation
                 β”‚
                 β–Ό
   POST /api/ticket  β†’  guardrails.screen(transcript)  β†’ blocked? 400, no ticket filed
                 β”‚
                 β–Ό
   phi_filter.redact_history()  (soft-mask email/phone/IP)
                 β”‚
                 β–Ό
   ticket_writer.build_ticket(team, history)
     LLM (temperature=0.2) turns raw comments into:
       { summary, description, priority }
                 β”‚
                 β–Ό
   okta_client.lookup_user(email)  β†’ requester name/department only
                 β”‚
                 β–Ό
   jira_client.create_ticket(...)  β†’ files in the team's Jira project
                 β”‚
                 β–Ό
   TicketResponse: { ticket_key, ticket_url, summary, description }
```

---

## πŸš€ Quick Start

```bash
git clone https://github.com/emran-Automation-Techlead/aria-service-desk-assistant.git
cd aria-service-desk-assistant
pip install -r requirements.txt
cp .env.example .env      # keep ARIA_MOCK_MODE=true to run fully offline
python main.py
```

Open **http://localhost:7862**

### Going live against real Jira / Confluence / Okta

Set `ARIA_MOCK_MODE=false` in `.env` and fill in:

```env
JIRA_BASE_URL=https://yourcompany.atlassian.net
JIRA_EMAIL=aria-bot@yourcompany.com
JIRA_API_TOKEN=...

CONFLUENCE_BASE_URL=https://yourcompany.atlassian.net
CONFLUENCE_EMAIL=aria-bot@yourcompany.com
CONFLUENCE_API_TOKEN=...

OKTA_DOMAIN=yourcompany.okta.com
OKTA_API_TOKEN=...
```

No code changes required β€” `confluence_client.py`, `jira_client.py`, and `okta_client.py` switch from the local JSON stores to live REST calls automatically. The vector index (`vector_store.py`) indexes whatever `confluence_client.search()` returns either way, so RAG retrieval works identically against mock or live content.

### Running the MCP server

```bash
python mcp_server.py
```

Point any MCP host (Claude Desktop, Claude Code) at this command to give it `search_knowledge_base`, `create_confluence_article`, `create_jira_ticket`, and `lookup_okta_user` as tools β€” each screened by the same guardrails as the chat UI.

---

## πŸ›‘οΈ Guardrails vs. soft redaction

Two distinct layers, on purpose:

- **`guardrails.py` β€” hard refusal.** Runs before anything reaches an LLM, Confluence, or Jira. Blocks outright: high-sensitivity PHI/PII (SSNs, card numbers, medical record numbers, via regex), sexual/nudity content (OpenAI Moderation API, scored against thresholds tuned tighter than the API's own `flagged` default β€” validated so ordinary IT phrasing like "kill the process" scores near zero), and security-risk requests (credential theft, bypassing MFA/auth, malware, exfiltration β€” a curated keyword heuristic plus the moderation model's illicit-content score, since phrasing like this isn't reliably caught by moderation alone).
- **`phi_filter.py` β€” soft redaction.** Low-sensitivity, operationally-necessary contact details (email, phone, IP) are masked with `[REDACTED-<TYPE>]` but the message still goes through β€” a ticket needs a reporter's email, a network issue needs an IP. API responses carry a `redactions` audit list (type + count).

---

## πŸ› οΈ Tech Stack

| Layer | Technology |
|---|---|
| **API** | FastAPI + Uvicorn, SSE streaming |
| **AI** | OpenAI (`gpt-4o-mini` by default, `temperature=0.2` everywhere) |
| **Orchestration** | LangChain (`langchain-openai` + `langchain-core`) for the intake-probing/reword loop |
| **RAG** | ChromaDB β€” chunked, embedded (`text-embedding-3-small`), confidence-scored retrieval |
| **Safety** | OpenAI Moderation API + custom PHI/PII and security-risk pattern checks |
| **Agent protocol** | MCP (`mcp` Python SDK, stdio transport) |
| **Integrations** | Jira Service Desk REST, Confluence Cloud REST, Okta REST β€” each with an offline JSON-backed mock mode |
| **Config** | Pydantic Settings v2 Β· python-dotenv |
| **UI** | Single-page vanilla HTML/JS chat widget β€” SSE consumer, musing status, blinking cursor |

---

## πŸ“„ License

MIT

Maintenance

ActivityMaintained
ResponsivenessNo issues