I built a personal prototype that checks whether an IT request can be fulfilled, needs approval, or should be declined. It is separate from my employer work.
I built a Teams-based assistant with four tools for IT requests. The AI interprets the request; separate rules decide what happens next. The demo uses sample data by default. Connecting it to live Microsoft Graph requires configuration.
I explored how clear rules and human approvals can guide an IT assistant.
I kept interpretation separate from permission. The AI identifies what the person wants. Fixed rules decide whether the request can proceed or needs approval.
Alex Thompson (Software Engineer I) asks for production deploy access. The copilot identifies him, checks the group, sees the elevated-approval policy, and routes the request to the right approver with the right context. One turn.
The agent's reasoning is the LLM. The access decision is not. Each layer does what it's best at — and the policy engine is what makes the system audit-defensible against a security review.
Reads the user's message in plain language, picks the right tool, refines arguments, asks clarifying questions if needed, drafts the final reply. What the LLM never does: approve access, override policy, or short-circuit the gate. Even if a prompt-injection attempt smuggles instructions, the runtime won't call request_access without a matching policy decision.
A pure-Python function with a documented decision table. Same input → same output → same trail of reasoning that ends up in the audit log. What the policy never does: understand natural language. It operates on resolved data — a user record, a group with its policy field, an optional KB article — and returns one of four outcomes plus a reason.
Tools are intentionally narrow. Each one does exactly one thing, returns a JSON-shaped result, and is composable with the others.
| Tool | Purpose | Side effects |
|---|---|---|
| lookup_user | Resolve a UPN to a user record — department, manager, group memberships, account status. Always the first call. | None |
| check_group_access | Read the group's policy (auto-on-join, manager-approval, compliance-approval, elevated-approval) and whether the user is already a member. | None |
| search_kb | Token-set scoring against title + tags + summary. Returns top articles. Used to answer self-service questions before considering a group change. | None |
| request_access | POST to Microsoft Graph's /groups/{id}/members/$ref. Only callable after the policy engine has returned auto_fulfill. | Mutates Graph |
The default mode is offline — deterministic router, fixture data, no API keys. Switch to Azure OpenAI + live Microsoft Graph with two env vars.
$ git clone https://github.com/iambrucedavis/it-support-copilot $ cd it-support-copilot $ python3 -m venv .venv && source .venv/bin/activate $ pip install -r requirements.txt # REPL mode — pick a user, ask questions $ python scripts/demo_chat.py --user bryan.jenkins@contoso.com # Replay a committed transcript $ python scripts/demo_chat.py --replay examples/conversations/vpn-self-serve.transcript.json
$ export GRAPH_MODE=live $ export COPILOT_MODE=azure $ export AZURE_OPENAI_ENDPOINT=https://your-aoi.openai.azure.com $ export AZURE_OPENAI_API_KEY=... $ az login # or service principal env vars $ uvicorn copilot_agent.server:app --port 8000 # /api/messages = Bot Framework webhook
Full Teams deployment (Bot Service registration, Container Apps, managed identity, Bicep templates) is documented in deploy/README.md. Sized for a low-volume internal pilot, scales to thousands of users by adjusting Container App replicas.