Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
30bea07
Add .env.example file for customer support configuration with placeho…
jbueza-railtownai May 21, 2026
7e2f46a
Add engine schema for customer support ticket sample (#5895)
jbueza-railtownai May 21, 2026
55818f5
Add pyproject.toml for customer support triage project setup with dep…
jbueza-railtownai May 21, 2026
ef79388
Add resolved and open ticket fixtures for customer support, including…
jbueza-railtownai May 21, 2026
32fd5fc
Add __init__.py for customer support module with versioning informati…
jbueza-railtownai May 21, 2026
acee0da
Add _env.py to load environment variables for CLI entrypoints in cust…
jbueza-railtownai May 21, 2026
b756127
Add README.md for customer support triage agent, detailing setup, usa…
jbueza-railtownai May 21, 2026
b12507c
Update .gitignore to exclude railtracks and uv.lock files (#5895)
jbueza-railtownai May 21, 2026
04550d8
Add shared async triage runner for customer support, implementing tic…
jbueza-railtownai May 21, 2026
e61ee51
Add customer support triage functionality with new modules for agent …
jbueza-railtownai May 21, 2026
cfe805d
Update README files to include details about the Customer Support Tri…
jbueza-railtownai May 21, 2026
df38eea
Update README.md to enhance the description of the Customer Support T…
jbueza-railtownai May 21, 2026
02a9dac
Refactor customer support triage project structure and enhance functi…
jbueza-railtownai May 21, 2026
af21ad7
Enhance customer support triage functionality by updating README with…
jbueza-railtownai May 21, 2026
c8dc879
Enhance Streamlit UI for customer support by adding a dialog for view…
jbueza-railtownai May 21, 2026
f1ff5b3
Update README.md to clarify the process of changing ticket status in …
jbueza-railtownai May 21, 2026
edd1fd8
Enhance customer support triage functionality by updating the README …
jbueza-railtownai May 21, 2026
3d63b6c
Update README.md to include optional debugging and visualization inst…
jbueza-railtownai May 21, 2026
a29639c
Implement chat functionality in TriageService for free-form conversat…
jbueza-railtownai May 21, 2026
732af50
Add chat functionality to Triage Agent interface, allowing users to i…
jbueza-railtownai May 21, 2026
aed2def
Refactor ticket status definition in ticket.py for improved readabili…
jbueza-railtownai May 25, 2026
258c7fd
Refactor argument parser formatting in webhook.py for improved readab…
jbueza-railtownai May 25, 2026
c140708
Refactor dashboard.py to improve code readability by formatting long …
jbueza-railtownai May 25, 2026
1d54a8e
Refactor ingest_page.py to improve code readability by formatting lon…
jbueza-railtownai May 25, 2026
e48eae7
Refactor triage_agent_page.py to improve code readability by formatti…
jbueza-railtownai May 25, 2026
6b3dccc
Refactor import statements in __init__.py to improve code readability…
jbueza-railtownai May 25, 2026
afca765
Refactor function definitions across multiple files to improve code r…
jbueza-railtownai May 25, 2026
2b1a937
Refactor TriageService docstring to improve readability by formatting…
jbueza-railtownai May 25, 2026
ffee5a2
Update README.md to include a section for Railengine Python examples …
jbueza-railtownai May 25, 2026
f2f2f06
Refactor controller package by updating the __init__.py docstring for…
jbueza-railtownai May 25, 2026
77de4d2
Remove unused CLI scripts from pyproject.toml and update package stru…
jbueza-railtownai May 25, 2026
99f1e43
Enhance customer support module by updating README for clearer ingest…
jbueza-railtownai May 25, 2026
c12b552
Refactor customer support module by replacing the Triage Agent page w…
jbueza-railtownai May 25, 2026
b1f99a3
Refactor Streamlit application by removing the render_config_button f…
jbueza-railtownai May 25, 2026
7319769
Refactor Streamlit UI components by removing left alignment logic fro…
jbueza-railtownai May 25, 2026
ad935a4
Implement search functionality in the customer support module by addi…
jbueza-railtownai May 25, 2026
9903f5f
Refactor ticket handling in the customer support module by removing t…
jbueza-railtownai May 25, 2026
7cde569
Refactor string formatting in dashboard and ingest page for improved …
jbueza-railtownai May 25, 2026
92febca
Remove .gitignore file from customer support module as it is no longe…
jbueza-railtownai May 25, 2026
182bfe1
Update README to clarify schema selection process for engine creation…
jbueza-railtownai May 25, 2026
c9db1fe
Update README to enhance clarity on search functionality and project …
jbueza-railtownai May 25, 2026
9abefbb
Update README to correct terminology from "Repo root" to "Repository …
jbueza-railtownai May 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,6 @@ dmypy.json

/CSharp/Examples/RailenginePoweredStatusPage/appsettings.Development.json


**/.railtracks
**/uv.lock
8 changes: 7 additions & 1 deletion Python/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
These samples use the Railengine Python SDK
# Railengine Python Examples

These samples use the Railengine Python SDK and [Railtracks](https://github.com/RailtownAI/railtracks).

## Examples

- **[Customer Support Triage Agent](customer-support/README.md)** — Ingest with `rail-engine-ingest`, search with `rail-engine`, triage with a Railtracks agent (dashboard + ingest UI; setup details are in `customer-support/README.md`).
15 changes: 15 additions & 0 deletions Python/customer-support/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Ingestion (from Project Settings → Engine Token)
ENGINE_TOKEN="[Your Engine Token]"

# Retrieval / search (PAT + Engine ID)
ENGINE_ID="[Your Engine ID]"
ENGINE_PAT="[Your Engine PAT]"

# Optional: override API host (must match the stack your ENGINE_TOKEN targets)
# RAILTOWN_API_URL="https://cndr.railtown.ai/api"

# Railtracks LLM (default: OpenAI — set one provider the Railtracks OpenAI/Anthropic integrations expect)
OPENAI_API_KEY="[Your OpenAI API key]"

# Optional: Anthropic instead of OpenAI (edit agent.py to use rt.llm.AnthropicLLM)
# ANTHROPIC_API_KEY="[Your Anthropic API key]"
103 changes: 103 additions & 0 deletions Python/customer-support/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Customer Support Triage

Demo stack: ingest support tickets into [Railengine](https://railengine.ai/), search resolved history (keyword index + `VectorStore1`), and triage a case with [Railtracks](https://github.com/RailtownAI/railtracks) structured output (priority, category, summaries, draft reply).

## Before you start

- A [Railengine](https://railengine.ai/) account plus a **new engine** configured with the sample schema in [`engine-schema.json`](engine-schema.json).
- Select that schema into your engine creation modal so documents match **`SupportTicket`**.
- Allowed ticket **`status`** values when ingesting vs. validating in-app: **`pending`**, **`open`**, **`in_progress`**, **`resolved`** — update long-lived engine/schema rules if yours differ before re-ingesting fixtures.
- Enable **Index** plus **VectorStore1** on fields such as `subject`, `body`, and `tags` in the Railengine console so search tools get useful hits beyond raw storage scans.

## Quick start

From `Python/customer-support/`:

```bash
cd Python/customer-support
cp .env.example .env # fill ENGINE_TOKEN, ENGINE_ID, ENGINE_PAT, OPENAI_API_KEY
uv sync
uv run streamlit run src/streamlit_app.py
```

## First demo flow

1. Open **Ingest**, load **`fixtures/tickets/ticket_001.json`**, and click **Ingest to Railengine**.
2. (Optional breadth) On **Ingest**, use sidebar **Seed all fixtures** to ingest every `fixtures/tickets/*.json`.
3. On **Ingest**, click **Run triage** on a single ticket, or open **Agent** → **Load queue** → **Triage all** for open and pending tickets.
4. Switch to **Dashboard**, click **Refresh board**, and browse the Kanban. Click a **card subject** to open ticket details in a dialog; change status from the card **dropdown** (**requires `ENGINE_TOKEN`** alongside list credentials).
5. Open **Search**, enter keywords (e.g. `billing invoice`), and press **Enter** or **🔍 Search** to query the keyword index. Select a row (or **📋 Details**) to open the ticket dialog.

## Debug and visualize the triage agent (optional)

After you run triage once (**Ingest** or **Agent**), inspect agent runs in the Railtracks UI:

```bash
cd Python/customer-support
railtracks update
railtracks viz
```

(`railtracks[visual]` is included in project dependencies; run `uv sync` if you have not already.)

Opens the local visualization app so you can debug tool calls, prompts, and structured output from the support triage flow.

## Environment variables

| Variable | Used for | Required when |
|----------|-----------|---------------|
| `ENGINE_TOKEN` | Ingest SDK | **Ingest** page · **Kanban status** dropdown |
| `ENGINE_PAT` | Retrieval / list / search | **Dashboard**, **Search**, triage tools |
| `ENGINE_ID` | Engine routing | **Dashboard**, **Search**, triage tools |
| `OPENAI_API_KEY` | Railtracks LLM | **Run triage** |

A local `.env` next to [`pyproject.toml`](pyproject.toml) is loaded automatically for Streamlit and the webhook receiver.

<details>
<summary>Optional: PII masking</summary>

If your engine masks sensitive fields after ingest, compare raw fixtures to stored docs in the dashboard to illustrate compliance-aware storage.</details>

## Project layout

Code is organized in layers so UI, business logic, and Railengine I/O stay separate. Imports use the **`customer_support`** package (`pyproject.toml` maps `src/` to that name).

```text
pages / controllers → services → repositories → rail-engine / rail-engine-ingest
models
agents (tools) → services or repositories
```

| Path | Role | Examples |
|------|------|----------|
| [`src/models/`](src/models/) | Pydantic domain types and constants | `SupportTicket`, `TriageAssessment`, `TicketPage` |
| [`src/repositories/`](src/repositories/) | SDK calls only (`Railengine`, `RailengineIngest` with `model=SupportTicket`) | `TicketRepository` — list, search, ingest, JSONPath |
| [`src/services/`](src/services/) | Use-case orchestration; pages call these, not the SDK | `IngestService`, `TicketListService`, `SearchService`, `TriageService` |
| [`src/agents/`](src/agents/) | Railtracks structured agent, chat agent, and tool nodes | `triage_agent.py`, `tools.py` (`search_similar_tickets`, …) |
| [`src/pages/`](src/pages/) | Streamlit screens (thin UI + session state) | `dashboard.py`, `search_page.py`, `ingest_page.py`, `agent_page.py` |
| [`src/controllers/`](src/controllers/) | Optional non-UI entry points | [`webhook.py`](src/controllers/webhook.py) — local publishing smoke test |
| [`src/config/`](src/config/) | Environment bootstrap | `ensure_dotenv_loaded()` loads `.env` next to `pyproject.toml` |
| [`src/streamlit_app.py`](src/streamlit_app.py) | App entry: `st.navigation` for Dashboard / Search / Ingest / Agent | `uv run streamlit run src/streamlit_app.py` |
| [`src/streamlit_common.py`](src/streamlit_common.py) | Shared UI (brand, toolbar, Kanban cards, ticket dialog, pipeline diagram) | Used across pages |

**Repository root (besides `src/`)**

- [`engine-schema.json`](engine-schema.json) — paste into the Railengine console when creating the engine
- [`fixtures/tickets/`](fixtures/tickets/) — sample `SupportTicket` JSON for ingest demos
- [`assets/`](assets/) — Streamlit branding (e.g. logo)
- [`.env.example`](.env.example) — required env var template

## Local only

**Do not** expose the Streamlit app on the public internet with live credentials unless you add authentication and hardening yourself.

## Optional: webhook receiver

Activation / publishing smoke test:

```bash
uv run python -m customer_support.controllers.webhook --port 8765
```

POST to `http://127.0.0.1:8765/webhook` (tunnel with ngrok if you need a public URL).
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions Python/customer-support/engine-schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"id": "ticket-sample-001",
"subject": "Unable to download invoice — billing portal error",
"body": "I'm logged in as finance@acmecorp.example but the invoice PDF returns 500. Our API key for the sandbox is sk-demo-INVALID-KEY-12345. Please call me at +1-555-0199.",
"status": "open",
"tags": ["billing", "portal"],
"createdAt": "2026-05-01T14:30:00Z",
"customerEmail": "finance@acmecorp.example",
"customerPhone": "+1-555-0199",
"productArea": "billing"
}
11 changes: 11 additions & 0 deletions Python/customer-support/fixtures/tickets/pending_001.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"id": "ticket-pending-001",
"subject": "Awaiting billing approval for quota increase",
"body": "Customer requested doubling API quota pending finance sign-off on the purchase order.",
"status": "pending",
"tags": ["billing", "quota"],
"createdAt": "2026-05-21T09:00:00Z",
"customerEmail": "ops-lead@vendor.example",
"customerPhone": "",
"productArea": "billing"
}
11 changes: 11 additions & 0 deletions Python/customer-support/fixtures/tickets/resolved_auth_001.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"id": "ticket-resolved-auth-001",
"subject": "Resolved: MFA enrollment loop for new hires",
"body": "Caused by stale device id in mobile app. Support had users reinstall app and re-enroll TOTP. No tenant-wide outage.",
"status": "resolved",
"tags": ["auth", "mfa", "mobile"],
"createdAt": "2026-03-18T14:00:00Z",
"customerEmail": "redacted@example.com",
"customerPhone": "",
"productArea": "authentication"
}
11 changes: 11 additions & 0 deletions Python/customer-support/fixtures/tickets/resolved_billing_001.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"id": "ticket-resolved-billing-001",
"subject": "Resolved: VAT ID missing on invoice export",
"body": "Customer could not download VAT-compliant PDFs. Fix was to add VAT ID in Billing → Company profile and clear CDN cache.",
"status": "resolved",
"tags": ["billing", "vat", "invoice"],
"createdAt": "2026-04-10T09:00:00Z",
"customerEmail": "redacted@example.com",
"customerPhone": "",
"productArea": "billing"
}
11 changes: 11 additions & 0 deletions Python/customer-support/fixtures/tickets/resolved_billing_002.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"id": "ticket-resolved-billing-002",
"subject": "Resolved: Portal 500 on invoice — stale session cookie",
"body": "User saw 500 on invoice PDF until cookies cleared. Documented workaround and shipped fix in portal 2.3.1.",
"status": "resolved",
"tags": ["billing", "portal", "cache"],
"createdAt": "2026-04-22T11:30:00Z",
"customerEmail": "redacted@example.com",
"customerPhone": "",
"productArea": "billing"
}
11 changes: 11 additions & 0 deletions Python/customer-support/fixtures/tickets/ticket_001.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"id": "ticket-open-001",
"subject": "Invoice PDF returns 500 in billing portal",
"body": "I'm logged in as finance@acmecorp.example but every invoice download fails with HTTP 500. Our sandbox integration key is sk-demo-INVALID-KEY-12345 — pasted from the dashboard. Please call me at +1-555-0199 if you need to debug live.",
"status": "open",
"tags": ["billing", "portal", "urgent"],
"createdAt": "2026-05-20T15:00:00Z",
"customerEmail": "finance@acmecorp.example",
"customerPhone": "+1-555-0199",
"productArea": "billing"
}
11 changes: 11 additions & 0 deletions Python/customer-support/fixtures/tickets/ticket_002.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"id": "ticket-open-002",
"subject": "SSO fails after password rotation — error AADSTS50076",
"body": "All users in our eu-west tenant see AADSTS50076 when signing in via SAML. Admin contact: it-lead@contoso.example. Mobile: +44 20 7946 0958. Happened right after we rotated IdP certs.",
"status": "in_progress",
"tags": ["auth", "sso", "azure-ad"],
"createdAt": "2026-05-20T16:20:00Z",
"customerEmail": "it-lead@contoso.example",
"customerPhone": "+44 20 7946 0958",
"productArea": "authentication"
}
33 changes: 33 additions & 0 deletions Python/customer-support/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"

[project]
name = "customer-support-triage"
version = "0.1.0"
description = "Customer support triage demo: Railengine ingest + search + Railtracks agent"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"rail-engine>=0.2.1",
"rail-engine-ingest>=0.2.1",
"railtracks[visual]>=1.3.0",
"pydantic>=2.0",
"python-dotenv>=1.0.0",
"streamlit>=1.57.0",
"watchdog>=6.0.0",
]

# Modules live directly under ``src/``; setuptools maps that tree to ``customer_support.*``.
[tool.setuptools]
package-dir = { "customer_support" = "src" }
packages = [
"customer_support",
"customer_support.agents",
"customer_support.config",
"customer_support.controllers",
"customer_support.models",
"customer_support.pages",
"customer_support.repositories",
"customer_support.services",
]
5 changes: 5 additions & 0 deletions Python/customer-support/src/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Customer support triage example: Railengine + Railtracks."""

__all__ = ["__version__"]

__version__ = "0.1.0"
5 changes: 5 additions & 0 deletions Python/customer-support/src/agents/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Railtracks agents."""

from customer_support.agents.triage_agent import build_triage_agent

__all__ = ["build_triage_agent"]
121 changes: 121 additions & 0 deletions Python/customer-support/src/agents/tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""Railtracks tool nodes backed by TicketRepository (no global SDK client)."""

from __future__ import annotations

import json
from typing import Any, Literal

import railtracks as rt

from customer_support.models.ticket import TICKET_STATUSES, SupportTicket
from customer_support.repositories import TicketRepository


def _ticket_to_brief(t: SupportTicket) -> dict[str, Any]:
return {
"id": t.id,
"subject": t.subject,
"status": t.status,
"tags": t.tags,
"productArea": t.productArea,
"createdAt": t.createdAt,
"body_excerpt": (t.body[:280] + "…") if len(t.body) > 280 else t.body,
}


@rt.function_node
async def search_similar_tickets(
query: str,
mode: Literal["vector", "index", "both"] = "both",
limit: int = 6,
) -> str:
"""
Search historical tickets in Railengine using keyword index and/or semantic vector search.

Args:
query: Natural language or keywords describing the issue (subject + symptoms).
mode: Use embedding search (vector), keyword index (index), or run both and merge.
limit: Max tickets to return per search mode (capped for latency).
"""
limit = max(1, min(int(limit), 25))
merged: dict[str, SupportTicket] = {}
repo = TicketRepository()

if mode in ("index", "both"):
for t in await repo.search_index_hits(query, limit):
merged[t.id] = t
if mode in ("vector", "both"):
for t in await repo.search_vector_hits(query, limit):
merged[t.id] = t

briefs = [_ticket_to_brief(t) for t in merged.values()]
return json.dumps(briefs, ensure_ascii=False, indent=2)


@rt.function_node
async def list_recent_tickets(status: str = "resolved", limit: int = 15) -> str:
"""
List recent tickets from hot storage. Filter client-side by status.

Args:
status: pending | open | in_progress | resolved — only matching tickets returned.
limit: Max rows to return (JSONPath query when supported; otherwise capped scan).
"""
repo = TicketRepository()
want = status.strip().lower()
valid = set(TICKET_STATUSES)
if want not in valid:
return json.dumps(
{"error": "status must be pending, open, in_progress, or resolved"},
indent=2,
)

cap = max(1, min(int(limit), 50))
collected: list[SupportTicket] = []

for t in await repo.query_jsonpath_tickets(f"$.status:{want}"):
collected.append(t)
if len(collected) >= cap:
break

if len(collected) < cap:
max_scan = 400
seen_ids = {t.id for t in collected}
async for t in repo.iter_storage_tickets(page_size=100, max_docs=max_scan):
if t.status != want:
continue
if t.id in seen_ids:
continue
seen_ids.add(t.id)
collected.append(t)
if len(collected) >= cap:
break

return json.dumps(
[_ticket_to_brief(t) for t in collected],
ensure_ascii=False,
indent=2,
)


@rt.function_node
async def get_ticket_by_id(ticket_id: str) -> str:
"""
Fetch a ticket by its business id (the `id` field inside the ingested JSON document).

Uses JSONPath storage query when available; falls back to a capped scan of recent pages.
"""
repo = TicketRepository()
tid = ticket_id.strip()
if not tid:
return json.dumps({"error": "ticket_id required"}, indent=2)

for t in await repo.query_jsonpath_tickets(f"$.id:{tid}"):
if t.id == tid:
return json.dumps(_ticket_to_brief(t), ensure_ascii=False, indent=2)

async for t in repo.iter_storage_tickets(page_size=100, max_docs=500):
if t.id == tid:
return json.dumps(_ticket_to_brief(t), ensure_ascii=False, indent=2)

return json.dumps({"error": f"ticket not found: {tid}"}, indent=2)
Loading