toga-ai 1.0.91 → 1.0.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/2.0/apps/voice-to-voice/INDEX.md +8 -0
- package/knowledge/2.0/apps/voice-to-voice/architecture.md +181 -0
- package/knowledge/2.0/apps/voice-to-voice/features/livekit-pipeline.md +147 -0
- package/knowledge/2.0/apps/voice-to-voice/features/post-call-lambda.md +177 -0
- package/knowledge/2.0/apps/voice-to-voice/features/ticket-api-integration.md +251 -0
- package/knowledge/INDEX.md +2 -0
- package/knowledge/clients/office-depot/INDEX.md +6 -0
- package/knowledge/clients/office-depot/features/odp-tech-support.md +144 -0
- package/knowledge/clients/office-depot/profile.md +89 -0
- package/knowledge/registry.json +2 -1
- package/package.json +1 -1
- package/rules/python/python.md +136 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# voice-to-voice (TOGa Voice) — 2.0 knowledge
|
|
2
|
+
|
|
3
|
+
| Doc | Summary | Files |
|
|
4
|
+
|-----|---------|-------|
|
|
5
|
+
| [TOGa Voice (voice-to-voice) Architecture](architecture.md) | **TOGa Voice** (`voice-to-voice`) is a multi-tenant **LiveKit Agents**–based phone voice agent platform. | voice-to-voice/core/base_config.py, voice-to-voice/core/utils.py, voice-to-voice/core/services/egress.py, voice-to-voice/core/services/knowledge_base.py, voice-to-voice/core/services/s3_upload.py, voice-to-voice/core/services/ticket_api.py, voice-to-voice/clients/odp/agent.py, voice-to-voice/clients/odp/agents/tech_support.py, voice-to-voice/clients/odp/config.yaml, voice-to-voice/clients/odp/prompts.py, voice-to-voice/clients/odp/Dockerfile, voice-to-voice/clients/odp/deploy.sh, voice-to-voice/clients/odp/livekit.toml, voice-to-voice/lambda_updated/lambda_function.py, voice-to-voice/docs/CLIENT_ONBOARDING_SOP.md, voice-to-voice/docs/inbound-call-routing-rd.md |
|
|
6
|
+
| [LiveKit Voice Pipeline — STT / LLM / TTS / VAD / Turn Detection](features/livekit-pipeline.md) | The active voice loop is **fully AWS-native**: Amazon Transcribe (STT) → Bedrock Claude (LLM) → Amazon Polly (TTS), with Silero VAD and LiveKit's multilingual t | voice-to-voice/clients/odp/agent.py, voice-to-voice/clients/odp/agents/tech_support.py, voice-to-voice/clients/odp/livekit.toml, voice-to-voice/clients/odp/Dockerfile, voice-to-voice/clients/odp/deploy.sh, voice-to-voice/clients/odp/pyproject.toml, voice-to-voice/core/services/egress.py, voice-to-voice/core/services/knowledge_base.py |
|
|
7
|
+
| [Post-Call Lambda — Transcript Conversion, Report, Ticket Attachment](features/post-call-lambda.md) | After every call, an AWS Lambda picks up the S3 ticket folder the agent left behind, converts the transcript to API format, generates a TXT report of the conver | voice-to-voice/lambda_updated/lambda_function.py, voice-to-voice/lambda_updated/api_client.py, voice-to-voice/lambda_updated/report_generator.py, voice-to-voice/lambda_updated/transcript_converter.py, voice-to-voice/lambda_updated/requirements.txt |
|
|
8
|
+
| [Agilant Ticket API — TOGa Desk 1.0 Integration](features/ticket-api-integration.md) | This is the **single seam** between the new Python voice agent and the legacy **TOGa Desk 1.0** PHP ticketing app. | voice-to-voice/core/services/ticket_api.py, voice-to-voice/lambda_updated/api_client.py, voice-to-voice/lambda_updated/lambda_function.py, voice-to-voice/clients/odp/agent.py, voice-to-voice/clients/odp/config.yaml, voice-to-voice/scripts/test_ticket_api.sh, voice-to-voice/docs/inbound-call-routing-rd.md |
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: TOGa Voice (voice-to-voice) Architecture
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: voice-to-voice
|
|
5
|
+
project: TOGa Voice
|
|
6
|
+
client: shared
|
|
7
|
+
type: architecture
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-16
|
|
10
|
+
owners: [akhokhani]
|
|
11
|
+
files:
|
|
12
|
+
- voice-to-voice/core/base_config.py
|
|
13
|
+
- voice-to-voice/core/utils.py
|
|
14
|
+
- voice-to-voice/core/services/egress.py
|
|
15
|
+
- voice-to-voice/core/services/knowledge_base.py
|
|
16
|
+
- voice-to-voice/core/services/s3_upload.py
|
|
17
|
+
- voice-to-voice/core/services/ticket_api.py
|
|
18
|
+
- voice-to-voice/clients/odp/agent.py
|
|
19
|
+
- voice-to-voice/clients/odp/agents/tech_support.py
|
|
20
|
+
- voice-to-voice/clients/odp/config.yaml
|
|
21
|
+
- voice-to-voice/clients/odp/prompts.py
|
|
22
|
+
- voice-to-voice/clients/odp/Dockerfile
|
|
23
|
+
- voice-to-voice/clients/odp/deploy.sh
|
|
24
|
+
- voice-to-voice/clients/odp/livekit.toml
|
|
25
|
+
- voice-to-voice/lambda_updated/lambda_function.py
|
|
26
|
+
- voice-to-voice/docs/CLIENT_ONBOARDING_SOP.md
|
|
27
|
+
- voice-to-voice/docs/inbound-call-routing-rd.md
|
|
28
|
+
related:
|
|
29
|
+
- features/livekit-pipeline.md
|
|
30
|
+
- features/ticket-api-integration.md
|
|
31
|
+
- features/post-call-lambda.md
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Summary
|
|
35
|
+
|
|
36
|
+
**TOGa Voice** (`voice-to-voice`) is a multi-tenant **LiveKit Agents**–based
|
|
37
|
+
phone voice agent platform. A shared `core/` layer provides config, AWS
|
|
38
|
+
service wrappers, and the Agilant Ticket API client; each tenant lives in
|
|
39
|
+
`clients/<slug>/` as its own container deployed to **LiveKit Cloud**. Today
|
|
40
|
+
the only deployed tenant is **Office Depot (ODP)** — a Tier-1 tech-support
|
|
41
|
+
agent for store associates, integrated with TOGa Desk 1.0 via the
|
|
42
|
+
**Agilant Ticket API**. A separate AWS **Lambda** processes post-call
|
|
43
|
+
artifacts (audio + report) and attaches them back to the ticket.
|
|
44
|
+
|
|
45
|
+
The platform sits under `2.0/apps/` for the same reason as **talos**: it's
|
|
46
|
+
Python (not PHP), but its consumers (callers of Office Depot stores) and
|
|
47
|
+
ticket destination (TOGa Desk 1.0) are 2.0-era TOGa surfaces; tooling and
|
|
48
|
+
conventions match the rest of the Python fleet.
|
|
49
|
+
|
|
50
|
+
## Top-level layout
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
voice-to-voice-new/
|
|
54
|
+
├── core/ # shared platform code
|
|
55
|
+
│ ├── base_config.py # pydantic-settings: YAML + env precedence
|
|
56
|
+
│ ├── utils.py # phone sanitization, SigV4 redaction
|
|
57
|
+
│ └── services/
|
|
58
|
+
│ ├── egress.py # LiveKit RoomCompositeEgress → S3 .ogg
|
|
59
|
+
│ ├── knowledge_base.py # Bedrock KB retrieve() + TTL cache
|
|
60
|
+
│ ├── s3_upload.py # raw .ogg → ticket folder; report JSON upload
|
|
61
|
+
│ └── ticket_api.py # Agilant Ticket API client (HMAC-SHA256)
|
|
62
|
+
├── clients/
|
|
63
|
+
│ └── odp/ # Office Depot deployment
|
|
64
|
+
│ ├── agent.py # LiveKit session entrypoint
|
|
65
|
+
│ ├── agents/tech_support.py # TalosAgent — tools + greeting
|
|
66
|
+
│ ├── config.yaml # non-secret client settings
|
|
67
|
+
│ ├── prompts.py # system prompt (call flow + output rules)
|
|
68
|
+
│ ├── Dockerfile # uv-based multi-stage build
|
|
69
|
+
│ ├── deploy.sh # copy core/ → build → `lk agent deploy`
|
|
70
|
+
│ └── livekit.toml # LiveKit Cloud project + agent id
|
|
71
|
+
├── lambda_updated/ # AWS Lambda post-call processor
|
|
72
|
+
│ ├── lambda_function.py # EventBridge/S3-triggered orchestrator
|
|
73
|
+
│ ├── api_client.py # PUT /ticket/{n} with HMAC
|
|
74
|
+
│ ├── report_generator.py # TXT report builder
|
|
75
|
+
│ └── transcript_converter.py # session_report.json → API transcript fmt
|
|
76
|
+
├── docs/
|
|
77
|
+
│ ├── CLIENT_ONBOARDING_SOP.md # 14-phase new-tenant runbook
|
|
78
|
+
│ ├── inbound-call-routing-rd.md # phone → LiveKit → agent flow
|
|
79
|
+
│ └── toga-voice-architecture.html
|
|
80
|
+
└── scripts/test_ticket_api.sh
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Multi-tenant model
|
|
84
|
+
|
|
85
|
+
- **One container per client.** Each `clients/<slug>/` is its own image
|
|
86
|
+
pushed to LiveKit Cloud as its own agent worker. They share a single AWS
|
|
87
|
+
account and a single S3 bucket; isolation is by **client-prefixed S3 keys**
|
|
88
|
+
(`odp/upload/...`) and per-client env (Bedrock KB id, ticket API keys).
|
|
89
|
+
- **`deploy.sh` copies `core/` into the client folder before build.** The
|
|
90
|
+
Dockerfile builds from the client directory; without the copy step `core/`
|
|
91
|
+
is missing in the image. Future improvement: install `core/` as a wheel.
|
|
92
|
+
- **Onboarding** (`docs/CLIENT_ONBOARDING_SOP.md`) is a copy-and-customize
|
|
93
|
+
flow, not a code-generation flow — copy `clients/odp/` to
|
|
94
|
+
`clients/<new>/`, edit prompts/config/env, deploy.
|
|
95
|
+
|
|
96
|
+
## Voice pipeline
|
|
97
|
+
|
|
98
|
+
The active pipeline is **fully AWS-native** (no Deepgram / Cartesia /
|
|
99
|
+
OpenAI):
|
|
100
|
+
|
|
101
|
+
| Stage | Plugin | Notes |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| STT | Amazon Transcribe (`en-US`) | Wrapped in `FallbackAdapter` (two instances) to survive Transcribe's 15s idle timeout |
|
|
104
|
+
| LLM | Bedrock Claude — Haiku primary, Sonnet fallback | `FallbackAdapter`, `temp=0.4`, `max_tokens=300` |
|
|
105
|
+
| TTS | Amazon Polly (`Matthew`, engine `generative`) | Preemptive generation for latency |
|
|
106
|
+
| VAD | Silero (downloaded at build) | Loaded in worker prewarm (`agent.py:115`) |
|
|
107
|
+
| Turn detection | LiveKit multilingual model | Endpointing 0.5–0.8s; interruption threshold 1 word |
|
|
108
|
+
|
|
109
|
+
Pipeline wiring is in `clients/odp/agent.py:162–222`. Plugin versions are
|
|
110
|
+
pinned via `livekit-agents[silero,turn-detector,aws]~=1.4`.
|
|
111
|
+
|
|
112
|
+
## Deployment
|
|
113
|
+
|
|
114
|
+
- **Runtime:** LiveKit Cloud agent worker (managed). No self-hosted infra.
|
|
115
|
+
- **Container:** multi-stage Dockerfile, uv-based, Python 3.12, non-root
|
|
116
|
+
user UID 10001. Silero VAD + turn-detector weights downloaded at build
|
|
117
|
+
time so cold starts don't fetch.
|
|
118
|
+
- **Entry:** `uv run agent.py start` (LiveKit Agents `WorkerOptions`).
|
|
119
|
+
- **Secrets:** synced via `lk agent update-secrets --secrets-file .env
|
|
120
|
+
--overwrite` before each deploy. `.env` never enters the image.
|
|
121
|
+
- **Rollback:** `lk agent rollback` and `lk agent logs`.
|
|
122
|
+
|
|
123
|
+
See `features/livekit-pipeline.md`.
|
|
124
|
+
|
|
125
|
+
## Integration with TOGa Desk 1.0 (Agilant Ticket API)
|
|
126
|
+
|
|
127
|
+
**Critical:** the voice agent does **not** call the standard TOGa Desk PHP
|
|
128
|
+
`/desk/api/` endpoint. It calls a separate **Agilant Ticket API** at
|
|
129
|
+
`https://api.agilantsolutions.com` (HMAC-SHA256 auth) which abstracts over
|
|
130
|
+
the underlying TOGa Desk 1.0 ticketing tables. The same API is hit twice:
|
|
131
|
+
|
|
132
|
+
1. **During the call** — `core/services/ticket_api.py` GETs by callback
|
|
133
|
+
number and work order for repeat-caller detection, then POSTs a new
|
|
134
|
+
ticket at end of call.
|
|
135
|
+
2. **After the call** — `lambda_updated/api_client.py` PUTs the transcript +
|
|
136
|
+
audio + TXT report to `/ticket/{number}` to attach artifacts.
|
|
137
|
+
|
|
138
|
+
Full contract, payload shapes, HMAC scheme, and PII considerations live in
|
|
139
|
+
`features/ticket-api-integration.md`. TOGa Desk PHP side: see
|
|
140
|
+
`1.0/apps/togadesk/architecture.md` for which underlying tables/controllers
|
|
141
|
+
carry this traffic.
|
|
142
|
+
|
|
143
|
+
## Storage
|
|
144
|
+
|
|
145
|
+
- **S3 bucket** `talos-voice-to-voice` (shared, client-prefixed):
|
|
146
|
+
- `odp/upload/raw/<session_id>.ogg` — LiveKit egress staging.
|
|
147
|
+
- `odp/upload/<ticketNo>_<sessionId>_<refId>/`
|
|
148
|
+
- `call_recording.ogg` (moved from `raw/`)
|
|
149
|
+
- `session_report.json` (agent-written; triggers Lambda)
|
|
150
|
+
- `odp/processed/...` — Lambda destination on success.
|
|
151
|
+
- `odp/unprocessable/...` — no ticket number extractable.
|
|
152
|
+
- **Bedrock Knowledge Base** — per-client KB id; documents uploaded to
|
|
153
|
+
`togaiq/<category>/<client_id>/archived/`.
|
|
154
|
+
- **No primary DB.** Ticket state lives in TOGa Desk via the Ticket API.
|
|
155
|
+
|
|
156
|
+
## Observability
|
|
157
|
+
|
|
158
|
+
- LiveKit Cloud logs (`lk agent logs`).
|
|
159
|
+
- Lambda → CloudWatch.
|
|
160
|
+
- **No Langfuse / OTEL today.** Adding them requires an `observability/`
|
|
161
|
+
module mirroring talos's pattern; not yet built. This is the largest
|
|
162
|
+
known gap.
|
|
163
|
+
|
|
164
|
+
## Key decisions
|
|
165
|
+
|
|
166
|
+
- **AWS-native voice stack** — Transcribe + Bedrock + Polly. No external
|
|
167
|
+
voice vendors. Simpler IAM, single billing surface; ties us to AWS voice
|
|
168
|
+
quality.
|
|
169
|
+
- **`core/` is copied into the build, not installed as a package.** Pragmatic
|
|
170
|
+
today, brittle if `core/` grows or two clients pin different versions.
|
|
171
|
+
- **Custom Ticket API**, not direct TOGa Desk PHP — gives a stable contract
|
|
172
|
+
decoupled from the PHP framework's internal schema. The Agilant Ticket
|
|
173
|
+
API is the single seam between voice and Desk 1.0.
|
|
174
|
+
- **Lambda post-processing**, not in-process. Keeps the agent worker hot;
|
|
175
|
+
post-call work is idempotent + retriable from S3 state.
|
|
176
|
+
- **One ticket per call invariant** — `ensure_ticket_created_for_call`
|
|
177
|
+
(`core/services/ticket_api.py`) guarantees creation even if the LLM
|
|
178
|
+
forgot to call the tool.
|
|
179
|
+
|
|
180
|
+
## Change history
|
|
181
|
+
- 2026-06-16 — Initial architecture doc for voice-to-voice (TOGa Voice), with ODP as the first tenant. (akhokhani)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: LiveKit Voice Pipeline — STT / LLM / TTS / VAD / Turn Detection
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: voice-to-voice
|
|
5
|
+
project: TOGa Voice
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-16
|
|
10
|
+
owners: [akhokhani]
|
|
11
|
+
files:
|
|
12
|
+
- voice-to-voice/clients/odp/agent.py
|
|
13
|
+
- voice-to-voice/clients/odp/agents/tech_support.py
|
|
14
|
+
- voice-to-voice/clients/odp/livekit.toml
|
|
15
|
+
- voice-to-voice/clients/odp/Dockerfile
|
|
16
|
+
- voice-to-voice/clients/odp/deploy.sh
|
|
17
|
+
- voice-to-voice/clients/odp/pyproject.toml
|
|
18
|
+
- voice-to-voice/core/services/egress.py
|
|
19
|
+
- voice-to-voice/core/services/knowledge_base.py
|
|
20
|
+
related:
|
|
21
|
+
- ../architecture.md
|
|
22
|
+
- ticket-api-integration.md
|
|
23
|
+
- post-call-lambda.md
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Summary
|
|
27
|
+
|
|
28
|
+
The active voice loop is **fully AWS-native**: Amazon Transcribe (STT) →
|
|
29
|
+
Bedrock Claude (LLM) → Amazon Polly (TTS), with Silero VAD and LiveKit's
|
|
30
|
+
multilingual turn detector. Wrapped in LiveKit Agents SDK v1.4. One worker
|
|
31
|
+
process per LiveKit Cloud agent; one `AgentSession` per call.
|
|
32
|
+
|
|
33
|
+
## Key files / entry points
|
|
34
|
+
|
|
35
|
+
| File | Purpose |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `clients/odp/agent.py` | Worker bootstrap: `prewarm`, `entrypoint`, plugin wiring (lines ~115, 162–222) |
|
|
38
|
+
| `clients/odp/agents/tech_support.py` | `TalosAgent(Agent)` subclass — tools, greeting, userdata shape |
|
|
39
|
+
| `clients/odp/livekit.toml` | `project.subdomain`, `agent.id` for LiveKit Cloud |
|
|
40
|
+
| `clients/odp/Dockerfile` | Multi-stage uv build; bakes Silero + turn-detector weights at build time |
|
|
41
|
+
| `clients/odp/deploy.sh` | Copies `core/` into build context, syncs secrets, runs `lk agent deploy` |
|
|
42
|
+
| `core/services/egress.py` | Starts `RoomCompositeEgress` → S3 .ogg; polls stop |
|
|
43
|
+
| `core/services/knowledge_base.py` | Async wrapper around Bedrock `retrieve()` with TTL cache |
|
|
44
|
+
|
|
45
|
+
## How it works
|
|
46
|
+
|
|
47
|
+
### Worker lifecycle
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
WorkerOptions(prewarm, entrypoint)
|
|
51
|
+
└─ prewarm: load Silero VAD model into shared process state
|
|
52
|
+
└─ entrypoint(JobContext):
|
|
53
|
+
ctx.connect()
|
|
54
|
+
derive callback_number/interaction_id from room name pattern
|
|
55
|
+
"call-_+<E164>_..."
|
|
56
|
+
AgentSession[CallUserdata](stt=…, llm=…, tts=…, vad=…, …)
|
|
57
|
+
session.start(room=ctx.room, agent=TalosAgent(...))
|
|
58
|
+
fire fetch_recent_tickets(callback_number) ← Agilant Ticket API
|
|
59
|
+
await session.say(greeting)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Plugin wiring (agent.py)
|
|
63
|
+
|
|
64
|
+
| Stage | Plugin |
|
|
65
|
+
|---|---|
|
|
66
|
+
| **STT** | `aws.STT(language="en-US")` × 2 inside a `FallbackAdapter`. Transcribe disconnects after 15 s of silence; the fallback keeps the call alive. |
|
|
67
|
+
| **LLM** | `aws.LLM(model=BEDROCK_HAIKU, temperature=0.4, max_tokens=300)` primary; `aws.LLM(model=BEDROCK_SONNET, …)` fallback. Both inside `FallbackAdapter`. |
|
|
68
|
+
| **TTS** | `aws.TTS(voice="Matthew", engine="generative")` with **preemptive generation** for lower TTFB. |
|
|
69
|
+
| **VAD** | `silero.VAD.load()` in `prewarm` (shared across jobs in the worker). |
|
|
70
|
+
| **Turn detection** | LiveKit `MultilingualModel()` — endpointing 0.5–0.8 s; interruption threshold 1 word. |
|
|
71
|
+
| **Noise suppression** | `livekit-plugins-noise-cancellation` (`BVCTelephony` profile for SIP calls). |
|
|
72
|
+
|
|
73
|
+
### Agent class
|
|
74
|
+
|
|
75
|
+
`TalosAgent` (`agents/tech_support.py`):
|
|
76
|
+
|
|
77
|
+
- Inherits `livekit.agents.Agent`.
|
|
78
|
+
- `instructions` = full system prompt loaded from `prompts.py`.
|
|
79
|
+
- `on_enter()` plays greeting + fires KB warmup.
|
|
80
|
+
- Tools (`@function_tool`):
|
|
81
|
+
- `set_caller_info(name, store_id, phone, workorder)` — incremental
|
|
82
|
+
update, called per field as provided.
|
|
83
|
+
- `search_knowledge_base(query)` — Bedrock KB lookup, `kb_max_results`
|
|
84
|
+
from config.
|
|
85
|
+
- `upsert_ticket_context(subject, summary, steps_tried, notes, priority)`
|
|
86
|
+
— populates `userdata` for end-of-call ticket creation.
|
|
87
|
+
- `link_existing_ticket(ticket_number, additional_notes)` — repeat
|
|
88
|
+
caller path; updates instead of creating.
|
|
89
|
+
- `mark_caller_as_customer()` — flag misrouted retail customer; short
|
|
90
|
+
flow.
|
|
91
|
+
- `end_call(farewell)` — speak goodbye then close room gracefully.
|
|
92
|
+
|
|
93
|
+
### Userdata
|
|
94
|
+
|
|
95
|
+
`CallUserdata` dataclass holds session-scoped state: `callback_number`,
|
|
96
|
+
`interaction_id`, `session_id`, `recent_tickets` (populated by background
|
|
97
|
+
`fetch_recent_tickets`), `caller_info`, `ticket_context`, `existing_ticket`,
|
|
98
|
+
`is_customer_misroute`. Lives on `session.userdata` and is consumed by the
|
|
99
|
+
post-call writer (see `post-call-lambda.md`).
|
|
100
|
+
|
|
101
|
+
### Audio recording (egress)
|
|
102
|
+
|
|
103
|
+
`core/services/egress.py` starts a `RoomCompositeEgress` at session start
|
|
104
|
+
writing to `s3://talos-voice-to-voice/odp/upload/raw/<session_id>.ogg`. At
|
|
105
|
+
end-of-call `s3_upload.py` moves the staged `.ogg` into the ticket folder
|
|
106
|
+
`odp/upload/<ticketNo>_<sessionId>_<refId>/call_recording.ogg`, then deletes
|
|
107
|
+
the staging file.
|
|
108
|
+
|
|
109
|
+
### Knowledge base
|
|
110
|
+
|
|
111
|
+
`core/services/knowledge_base.py` wraps Bedrock `retrieve()` with an
|
|
112
|
+
in-process TTL cache (`kb_cache_ttl_seconds`, `kb_cache_max_entries` from
|
|
113
|
+
config). Cache key is the query string. The agent's `search_knowledge_base`
|
|
114
|
+
tool consults this.
|
|
115
|
+
|
|
116
|
+
## Data model
|
|
117
|
+
|
|
118
|
+
No DB. Per-call state: `session.userdata` (in memory). Persisted state: the
|
|
119
|
+
S3 ticket folder + the Agilant Ticket API ticket record.
|
|
120
|
+
|
|
121
|
+
## Client variations
|
|
122
|
+
|
|
123
|
+
`config.yaml` is the seam. Per-client overrides: `client_id`, `client_name`,
|
|
124
|
+
`agent_name`, `s3_upload_prefix`, `tts_voice`, `llm_max_tokens`,
|
|
125
|
+
`kb_max_results`, `timezone`. `prompts.py` is per-client. `core/` is shared.
|
|
126
|
+
|
|
127
|
+
## Gotchas / known issues
|
|
128
|
+
|
|
129
|
+
- **Transcribe 15 s idle timeout** drops the connection silently — only the
|
|
130
|
+
`FallbackAdapter` saves the call. Do not collapse it to a single
|
|
131
|
+
`aws.STT(...)`.
|
|
132
|
+
- **`deploy.sh` MUST copy `core/` into the client folder** before
|
|
133
|
+
`lk agent deploy`; otherwise import errors at boot. Most common deploy
|
|
134
|
+
fail.
|
|
135
|
+
- **Silero weights are baked at build time.** If the upstream URL changes,
|
|
136
|
+
the build breaks. Pin a known-good `livekit-plugins-silero` version.
|
|
137
|
+
- **Polly generative TTS** is region-restricted. Stay in `us-east-1`
|
|
138
|
+
unless you check availability.
|
|
139
|
+
- **Room name parsing** for caller phone is regex-based on
|
|
140
|
+
`call-_+<E164>_...` — if LiveKit changes its SIP room naming, identity
|
|
141
|
+
lookup silently breaks. Guard with a sanity check in `entrypoint`.
|
|
142
|
+
- **`asyncio.CancelledError` on user hangup** must propagate out of tools
|
|
143
|
+
without writing partial state — `userdata` is the source of truth for the
|
|
144
|
+
post-call ticket; partial writes leak.
|
|
145
|
+
|
|
146
|
+
## Change history
|
|
147
|
+
- 2026-06-16 — Initial LiveKit pipeline feature doc. (akhokhani)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Post-Call Lambda — Transcript Conversion, Report, Ticket Attachment
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: voice-to-voice
|
|
5
|
+
project: TOGa Voice
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-16
|
|
10
|
+
owners: [akhokhani]
|
|
11
|
+
files:
|
|
12
|
+
- voice-to-voice/lambda_updated/lambda_function.py
|
|
13
|
+
- voice-to-voice/lambda_updated/api_client.py
|
|
14
|
+
- voice-to-voice/lambda_updated/report_generator.py
|
|
15
|
+
- voice-to-voice/lambda_updated/transcript_converter.py
|
|
16
|
+
- voice-to-voice/lambda_updated/requirements.txt
|
|
17
|
+
related:
|
|
18
|
+
- ../architecture.md
|
|
19
|
+
- livekit-pipeline.md
|
|
20
|
+
- ticket-api-integration.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
|
|
25
|
+
After every call, an AWS Lambda picks up the S3 ticket folder the agent
|
|
26
|
+
left behind, converts the transcript to API format, generates a TXT
|
|
27
|
+
report of the conversation, and `PUT /ticket/{number}` on the Agilant
|
|
28
|
+
Ticket API to attach audio + report + transcript. On success the folder
|
|
29
|
+
is moved from `upload/` to `processed/`; on failure it stays in place
|
|
30
|
+
for the next tick to retry.
|
|
31
|
+
|
|
32
|
+
## Key files / entry points
|
|
33
|
+
|
|
34
|
+
| File | Role |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `lambda_function.py` | Lambda handler. Resolves trigger mode, lists candidate folders, drives the per-folder pipeline, moves the folder on success |
|
|
37
|
+
| `transcript_converter.py` | `convert_session_to_transcript(session_report)` — flat transcript → API format with synthetic incrementing timestamps; legacy fallbacks |
|
|
38
|
+
| `report_generator.py` | `build_report(session_report)` — TXT report: header, exec summary, extracted identifiers, repeated-opener detection, full transcript |
|
|
39
|
+
| `api_client.py` | `update_ticket(ticket_number, transcript, files)` — `PUT /ticket/{n}` with HMAC; base64-encoded files in payload |
|
|
40
|
+
| `requirements.txt` | boto3, requests (sync — this is a Lambda, not an async server) |
|
|
41
|
+
|
|
42
|
+
## How it works
|
|
43
|
+
|
|
44
|
+
### Triggers (4 modes)
|
|
45
|
+
|
|
46
|
+
| Mode | Event |
|
|
47
|
+
|---|---|
|
|
48
|
+
| Scheduled | EventBridge every 60 s — scans `<prefix>/upload/` (default `odp/upload/`) |
|
|
49
|
+
| S3 event | Object-created on `session_report.json` |
|
|
50
|
+
| Manual single | `{"folder_path": "odp/upload/<folder>"}` |
|
|
51
|
+
| Manual batch | `{"folders": [...], "client_id": "odp"}` |
|
|
52
|
+
|
|
53
|
+
### Per-folder pipeline
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
1. Parse ticket number from folder name `<ticketNo>_<sessionId>_<refId>`
|
|
57
|
+
(regex `^(\d+)_`); fail → move to <prefix>/unprocessable/.
|
|
58
|
+
2. Download <folder>/session_report.json.
|
|
59
|
+
3. If <folder>/call_recording.ogg missing, recover from
|
|
60
|
+
<prefix>/upload/raw/<session_id>.ogg.
|
|
61
|
+
4. transcript = transcript_converter.convert_session_to_transcript(session_report)
|
|
62
|
+
5. report_txt = report_generator.build_report(session_report)
|
|
63
|
+
6. files = [
|
|
64
|
+
{filename: "session_report.txt", content: b64(report_txt), mimetype: "text/plain"},
|
|
65
|
+
{filename: "call_recording.ogg", content: b64(audio_bytes), mimetype: "audio/ogg"}
|
|
66
|
+
]
|
|
67
|
+
7. api_client.update_ticket(ticket_number, transcript, files)
|
|
68
|
+
─ PUT /ticket/{number} (HMAC-signed)
|
|
69
|
+
8. On 2xx: move folder from upload/ → processed/.
|
|
70
|
+
On non-2xx: log; folder stays in upload/ for next tick.
|
|
71
|
+
9. Delete <prefix>/upload/raw/EG_*.json LiveKit egress metadata always.
|
|
72
|
+
Warn on stranded AJ_*.ogg without a matching folder.
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### `session_report.json` shape (input)
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"ticket_number": "3066759",
|
|
80
|
+
"ticket_reference_id": "<UUID>",
|
|
81
|
+
"interaction_id": "...",
|
|
82
|
+
"session_id": "AJ_...",
|
|
83
|
+
"room_name": "...",
|
|
84
|
+
"callback_number": "+1...",
|
|
85
|
+
"customer_name": "...",
|
|
86
|
+
"store_id": "...",
|
|
87
|
+
"workorder": "...",
|
|
88
|
+
"transcript": [{"role": "user|assistant", "text": "..."}, ...],
|
|
89
|
+
"ended_at": "YYYY-MM-DD HH:MM:SS TZ"
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Transcript conversion
|
|
94
|
+
|
|
95
|
+
`convert_session_to_transcript(report)`:
|
|
96
|
+
|
|
97
|
+
- Source: `report["transcript"]` (list of `{role, text}`).
|
|
98
|
+
- Output: list of `{role, text, timestamp}` with **synthetic** incrementing
|
|
99
|
+
timestamps (`1.0, 2.0, ...`). Real timestamps are not captured today;
|
|
100
|
+
this is a known gap.
|
|
101
|
+
- **Legacy fallbacks:** if `transcript` is absent, falls back to
|
|
102
|
+
`events[]` (older LiveKit format) or `chat_history`. Keep these
|
|
103
|
+
branches — old recordings in S3 still depend on them.
|
|
104
|
+
|
|
105
|
+
### Report generator
|
|
106
|
+
|
|
107
|
+
`build_report(report)` → plain-text:
|
|
108
|
+
|
|
109
|
+
- Header: ticket number, session id, caller info (name, callback, store,
|
|
110
|
+
workorder).
|
|
111
|
+
- Executive summary: user/assistant/total turn counts.
|
|
112
|
+
- Extracted identifiers: phone numbers and numeric tokens via regex
|
|
113
|
+
(best-effort; used to spot work-order numbers).
|
|
114
|
+
- Conversation quality: detection of repeated assistant openers (a sign
|
|
115
|
+
of scripty / stuck prompts).
|
|
116
|
+
- Full transcript: line-by-line with synthetic timestamps.
|
|
117
|
+
|
|
118
|
+
Legacy report shape also computes latency metrics (TTS TTFB, LLM TTFT,
|
|
119
|
+
end-to-end) and tool-call stats / interruptions — kept for backward
|
|
120
|
+
compatibility with older session reports.
|
|
121
|
+
|
|
122
|
+
### API client
|
|
123
|
+
|
|
124
|
+
Same Agilant Ticket API as the agent worker — see
|
|
125
|
+
`ticket-api-integration.md` for the HMAC scheme and key env-var names.
|
|
126
|
+
Endpoint: `PUT {API_BASE_URL}/ticket/{ticket_number}`.
|
|
127
|
+
|
|
128
|
+
## Data model
|
|
129
|
+
|
|
130
|
+
No DB. Lambda is **pure transformation** over S3 + ticket API state. The
|
|
131
|
+
S3 folder is the durable buffer between the agent and the ticket system.
|
|
132
|
+
|
|
133
|
+
## Client variations
|
|
134
|
+
|
|
135
|
+
Per-client env in Lambda configuration (no YAML in Lambda):
|
|
136
|
+
|
|
137
|
+
- `API_BASE_URL`, `<CLIENT>_API_PUBLIC_KEY`, `<CLIENT>_API_SECRET_KEY`
|
|
138
|
+
- `MOVE_TO_PROCESSED` — when false, leave folders in place after success
|
|
139
|
+
(debug mode).
|
|
140
|
+
- `S3_BUCKET`, default `talos-voice-to-voice`.
|
|
141
|
+
- `CLIENT_PREFIXES` — list of S3 prefixes to scan (`["odp/upload"]`
|
|
142
|
+
today; add new client prefixes here).
|
|
143
|
+
|
|
144
|
+
## Failure modes / retries
|
|
145
|
+
|
|
146
|
+
- **No automatic in-Lambda retries.** A non-2xx leaves the folder in
|
|
147
|
+
`upload/`; the next EventBridge tick retries from scratch. Intentional
|
|
148
|
+
— Lambda execution time is metered.
|
|
149
|
+
- **Missing `session_report.json`** → log and skip (audio without
|
|
150
|
+
metadata is unrecoverable).
|
|
151
|
+
- **Missing `call_recording.ogg`** → attempt recovery from
|
|
152
|
+
`<prefix>/upload/raw/<session_id>.ogg`; if still missing, attach the
|
|
153
|
+
report only.
|
|
154
|
+
- **No ticket number extractable** → folder moves to `unprocessable/`.
|
|
155
|
+
Manual review required.
|
|
156
|
+
- **`EG_*.json` egress metadata** always deleted, regardless of success.
|
|
157
|
+
Stranded `AJ_*.ogg` (raw recordings with no matching folder) get a
|
|
158
|
+
warn log; not auto-cleaned.
|
|
159
|
+
|
|
160
|
+
## Gotchas / known issues
|
|
161
|
+
|
|
162
|
+
- **The 60 s EventBridge tick is the only retry loop.** A persistent
|
|
163
|
+
4xx from the API will keep failing; nothing escalates. Add a
|
|
164
|
+
CloudWatch alarm on `upload/` folder age if this becomes operational
|
|
165
|
+
pain.
|
|
166
|
+
- **Synthetic timestamps in the transcript are not real.** Downstream
|
|
167
|
+
consumers (QA review, future Langfuse-style analysis) should not treat
|
|
168
|
+
them as wall-clock. Real timestamps would have to be captured at
|
|
169
|
+
source in the agent.
|
|
170
|
+
- **Sync `requests` here, async `httpx` in the agent.** Different libs,
|
|
171
|
+
same HMAC code path — keep the signer copy-paste-correct or pull it
|
|
172
|
+
into a shared package.
|
|
173
|
+
- **Lambda is on `requests`** — when porting to async, the HMAC signer
|
|
174
|
+
byte handling (raw HMAC → base64) must be preserved exactly.
|
|
175
|
+
|
|
176
|
+
## Change history
|
|
177
|
+
- 2026-06-16 — Initial post-call-lambda feature doc. (akhokhani)
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Agilant Ticket API — TOGa Desk 1.0 Integration
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: voice-to-voice
|
|
5
|
+
project: TOGa Voice
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-16
|
|
10
|
+
owners: [akhokhani]
|
|
11
|
+
files:
|
|
12
|
+
- voice-to-voice/core/services/ticket_api.py
|
|
13
|
+
- voice-to-voice/lambda_updated/api_client.py
|
|
14
|
+
- voice-to-voice/lambda_updated/lambda_function.py
|
|
15
|
+
- voice-to-voice/clients/odp/agent.py
|
|
16
|
+
- voice-to-voice/clients/odp/config.yaml
|
|
17
|
+
- voice-to-voice/scripts/test_ticket_api.sh
|
|
18
|
+
- voice-to-voice/docs/inbound-call-routing-rd.md
|
|
19
|
+
related:
|
|
20
|
+
- ../architecture.md
|
|
21
|
+
- livekit-pipeline.md
|
|
22
|
+
- post-call-lambda.md
|
|
23
|
+
- ../../../1.0/apps/togadesk/architecture.md
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Summary
|
|
27
|
+
|
|
28
|
+
This is the **single seam** between the new Python voice agent and the
|
|
29
|
+
legacy **TOGa Desk 1.0** PHP ticketing app. The voice agent does **not**
|
|
30
|
+
talk to the TOGa Desk `/desk/api/index.php` PHP endpoint directly. It talks
|
|
31
|
+
to a separate, narrow **Agilant Ticket API** at
|
|
32
|
+
`https://api.agilantsolutions.com` that abstracts ticket
|
|
33
|
+
create/read/update over TOGa Desk's internal tables.
|
|
34
|
+
|
|
35
|
+
Two write paths, one read path:
|
|
36
|
+
|
|
37
|
+
1. **Read (during call):** `GET /ticket/callbacknumber/{phone}` and
|
|
38
|
+
`GET /ticket/workorder/{workorder}` for repeat-caller detection.
|
|
39
|
+
2. **Write (end of call, from agent worker):** `POST /ticket` to create
|
|
40
|
+
the ticket from accumulated `session.userdata`.
|
|
41
|
+
3. **Write (post-call, from Lambda):** `PUT /ticket/{ticket_number}` to
|
|
42
|
+
attach transcript + audio + TXT report.
|
|
43
|
+
|
|
44
|
+
## Key files / entry points
|
|
45
|
+
|
|
46
|
+
| File | Role |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `core/services/ticket_api.py` | Agent-side HTTP client (21 KB). GETs, POST, HMAC signer, retry, dedup scoring, `ensure_ticket_created_for_call` |
|
|
49
|
+
| `lambda_updated/api_client.py` | Lambda-side HTTP client. PUT with base64-encoded files (audio + report) |
|
|
50
|
+
| `lambda_updated/lambda_function.py` | Orchestrator — reads `session_report.json`, calls `api_client.update_ticket(...)` |
|
|
51
|
+
| `clients/odp/agent.py` | Fires `fetch_recent_tickets(callback_number)` async at session start |
|
|
52
|
+
| `scripts/test_ticket_api.sh` | Smoke test against the prod API |
|
|
53
|
+
|
|
54
|
+
## How it works
|
|
55
|
+
|
|
56
|
+
### Authentication — HMAC-SHA256
|
|
57
|
+
|
|
58
|
+
Every request to `api.agilantsolutions.com` is signed:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
ts = utcnow().isoformat() # ISO-8601
|
|
62
|
+
sig = HMAC-SHA256(secret_key, ts) # raw bytes
|
|
63
|
+
auth_hdr = f"{public_key}.{base64(sig)}"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Headers on every call:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
Authorization: {public_key}.{b64sig}
|
|
70
|
+
Timestamp: {ts}
|
|
71
|
+
Content-Type: application/json
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Keys live in env (never in repo):
|
|
75
|
+
|
|
76
|
+
- Agent worker: `ODP_API_PUBLIC_KEY`, `ODP_API_SECRET_KEY` (read by
|
|
77
|
+
`base_config.py` → `core.utils.get_config()`).
|
|
78
|
+
- Lambda: same env var names, loaded from Lambda config.
|
|
79
|
+
|
|
80
|
+
`ticket_api.py:30–57` implements the signer; replicate exactly in any new
|
|
81
|
+
caller. **Never** invent a new signing scheme.
|
|
82
|
+
|
|
83
|
+
### 1) Inbound — repeat-caller detection (`GET`)
|
|
84
|
+
|
|
85
|
+
At session start `clients/odp/agent.py:235–249` fires (background) via
|
|
86
|
+
`asyncio.create_task`:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
GET /ticket/callbacknumber/{digits} # 10-digit, no country code
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Implementation: `ticket_api.fetch_recent_tickets()` (lines 85–143). Result
|
|
93
|
+
lands on `session.userdata.recent_tickets`.
|
|
94
|
+
|
|
95
|
+
For work-order validation (during the conversation):
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
GET /ticket/workorder/{workorder}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`ticket_api.validate_work_order(workorder, store_id)` (lines 182–281)
|
|
102
|
+
returns `{store_match, expected_store, related_tickets}`.
|
|
103
|
+
|
|
104
|
+
A composite scorer
|
|
105
|
+
`fetch_recent_tickets_by_store_workorder(number, store_id, workorder)`
|
|
106
|
+
ranks results as **exact / store_only / phone_only** to detect a true
|
|
107
|
+
repeat caller vs. shared-line noise.
|
|
108
|
+
|
|
109
|
+
### 2) End-of-call — ticket creation (`POST`)
|
|
110
|
+
|
|
111
|
+
`ticket_api.py:378–435` builds the payload from `session.userdata`:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"ticket_reference_id": "<UUID>",
|
|
116
|
+
"customer_name": "...",
|
|
117
|
+
"callback_number": "+1...",
|
|
118
|
+
"store_id": "...",
|
|
119
|
+
"workorder": "...",
|
|
120
|
+
"ticket_subject": "...",
|
|
121
|
+
"issue_summary": "...",
|
|
122
|
+
"steps_tried": "...",
|
|
123
|
+
"notes_for_human": "...",
|
|
124
|
+
"reason_to_transfer_human": "...",
|
|
125
|
+
"transcript": [{"role": "user|assistant", "text": "..."}, ...],
|
|
126
|
+
"status": "open",
|
|
127
|
+
"priority": "normal|high|low|critical",
|
|
128
|
+
"created_at": "<ISO-8601>",
|
|
129
|
+
"updated_at": "<ISO-8601>"
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Response: `{success: bool, ticketNumber: "<numeric string>", ...}`.
|
|
134
|
+
|
|
135
|
+
**Retry policy:** 2 retries on 5xx with exponential backoff (lines
|
|
136
|
+
417–420). 4xx fails fast — never retry a rejection.
|
|
137
|
+
|
|
138
|
+
**One-ticket-per-call invariant.**
|
|
139
|
+
`ensure_ticket_created_for_call(userdata, transcript)` (last thing the
|
|
140
|
+
worker does before shutdown) creates a ticket from derived fields if the
|
|
141
|
+
LLM forgot to call `upsert_ticket_context`. **Always called.** Removing
|
|
142
|
+
this guarantee leaks call data into S3 with no destination.
|
|
143
|
+
|
|
144
|
+
### 3) Post-call — attach artifacts (`PUT`)
|
|
145
|
+
|
|
146
|
+
`lambda_updated/api_client.py:113–213`:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
PUT /ticket/{ticket_number}
|
|
150
|
+
{
|
|
151
|
+
"transcript": [{"role": "user|assistant", "text": "..."}, ...],
|
|
152
|
+
"files": [
|
|
153
|
+
{"filename": "call_recording.ogg", "content": "<base64>", "mimetype": "audio/ogg"},
|
|
154
|
+
{"filename": "session_report.txt", "content": "<base64>", "mimetype": "text/plain"}
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Same HMAC scheme. Ticket number comes from the S3 folder name
|
|
160
|
+
`<ticketNo>_<sessionId>_<refId>` (regex `^(\d+)_`). No retries in Lambda
|
|
161
|
+
today — failure leaves the folder in `odp/upload/` and the next
|
|
162
|
+
EventBridge tick re-tries it from S3 state.
|
|
163
|
+
|
|
164
|
+
## How it lands in TOGa Desk 1.0
|
|
165
|
+
|
|
166
|
+
The Agilant Ticket API is the seam — but it ultimately writes to the same
|
|
167
|
+
underlying TOGa Desk tables (`TOGaDeskSupport.tickets` and related). From
|
|
168
|
+
the 1.0 togadesk docs the inferred mapping is:
|
|
169
|
+
|
|
170
|
+
| Voice payload field | Desk side |
|
|
171
|
+
|---|---|
|
|
172
|
+
| `ticket_reference_id` | UUID column (looked up for idempotency) |
|
|
173
|
+
| `callback_number`, `customer_name` | Customer fields on the ticket row |
|
|
174
|
+
| `ticket_subject`, `issue_summary`, `steps_tried` | Ticket body / description fields |
|
|
175
|
+
| `transcript` | Attached as a transcript record or note |
|
|
176
|
+
| `files[]` (PUT) | Ticket attachments (audio + TXT report) |
|
|
177
|
+
| `status`, `priority` | Direct columns |
|
|
178
|
+
| `store_id`, `workorder` | Custom fields on the ODP ticket form |
|
|
179
|
+
|
|
180
|
+
The voice agent never touches TOGa Desk's `App_Controller_*` PHP code. If
|
|
181
|
+
the Desk-side schema or controllers move, only the Agilant Ticket API
|
|
182
|
+
adapter has to change — the voice agent contract is stable.
|
|
183
|
+
|
|
184
|
+
## Data model
|
|
185
|
+
|
|
186
|
+
No local persistence in voice-to-voice. The Ticket API + S3 ticket folder
|
|
187
|
+
are the durable record:
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
S3: odp/upload/<ticketNo>_<sessionId>_<refId>/
|
|
191
|
+
├─ session_report.json (agent-written; ticket payload mirror)
|
|
192
|
+
├─ call_recording.ogg
|
|
193
|
+
└─ session_report.txt (Lambda-generated)
|
|
194
|
+
|
|
195
|
+
API: GET /ticket/callbacknumber/{phone}
|
|
196
|
+
GET /ticket/workorder/{wo}
|
|
197
|
+
POST /ticket
|
|
198
|
+
PUT /ticket/{number}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Client variations
|
|
202
|
+
|
|
203
|
+
Per-client config in `config.yaml` + env:
|
|
204
|
+
|
|
205
|
+
- `ticket_api_base_url` (override; default `https://api.agilantsolutions.com`)
|
|
206
|
+
- `ticket_api_timeout` (default 10 s)
|
|
207
|
+
- `<CLIENT>_API_PUBLIC_KEY`, `<CLIENT>_API_SECRET_KEY` env var pair
|
|
208
|
+
(ODP today; future clients get their own pair).
|
|
209
|
+
|
|
210
|
+
All clients use the same HMAC scheme and the same endpoint paths. Only
|
|
211
|
+
the keys differ.
|
|
212
|
+
|
|
213
|
+
## Trust boundary / security
|
|
214
|
+
|
|
215
|
+
- **HMAC of timestamp only — body is not signed.** Replay is bounded by
|
|
216
|
+
the server-side timestamp window; tampering is mitigated by HTTPS. Do
|
|
217
|
+
not weaken the timestamp check server-side.
|
|
218
|
+
- **PII in payload.** Transcripts carry customer names, phone numbers,
|
|
219
|
+
store IDs, and any spoken work-order numbers. Treat
|
|
220
|
+
`session_report.json` and the `transcript` field as PII at rest.
|
|
221
|
+
- **Keys never in repo.** `.env.example` shows the variable names; values
|
|
222
|
+
live only in LiveKit Cloud secrets and Lambda config. The
|
|
223
|
+
`RedactSigV4` helper in `core/utils.py` redacts AWS signing headers
|
|
224
|
+
from logs; confirm the ticket-API `Authorization` header is also
|
|
225
|
+
redacted before adding any new logging.
|
|
226
|
+
- **No 4xx retry.** Forbidding 4xx retries is what protects against
|
|
227
|
+
permission-misconfig cascades — keep it that way.
|
|
228
|
+
- **Test keys.** `scripts/test_ticket_api.sh` runs against the same prod
|
|
229
|
+
API; rotate its keys if they ever leak.
|
|
230
|
+
|
|
231
|
+
## Gotchas / known issues
|
|
232
|
+
|
|
233
|
+
- **Phone normalization is required everywhere.** API expects 10-digit US
|
|
234
|
+
(`2017366218`), not `+12017366218`. Always run
|
|
235
|
+
`core/utils.sanitize_phone()` before constructing a URL.
|
|
236
|
+
- **`ticket_reference_id` must be a stable UUID generated client-side**
|
|
237
|
+
for the lifetime of the call. If two retries generate different UUIDs
|
|
238
|
+
the server creates duplicate tickets.
|
|
239
|
+
- **`session_report.json` MUST be written last** in the agent shutdown
|
|
240
|
+
sequence — its presence in S3 is what triggers Lambda. Writing it
|
|
241
|
+
before the audio is moved into the ticket folder races the Lambda.
|
|
242
|
+
- **The Agilant Ticket API is private** — no public docs, no OpenAPI.
|
|
243
|
+
The shape lives in `ticket_api.py` and is the canonical reference.
|
|
244
|
+
Update this doc when the payload changes.
|
|
245
|
+
- **TOGa Desk PHP changes can silently break the API contract.** If a
|
|
246
|
+
Desk-side migration renames a column the Agilant API maps to, POSTs may
|
|
247
|
+
succeed but read-backs miss data. Coordinate cross-team for any
|
|
248
|
+
`tickets` table change in 1.0.
|
|
249
|
+
|
|
250
|
+
## Change history
|
|
251
|
+
- 2026-06-16 — Initial ticket-api-integration feature doc — the TOGa Desk 1.0 ↔ voice-to-voice seam. (akhokhani)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -23,6 +23,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
23
23
|
- **toga2-view** (TOGa View Frontend) — 0 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
|
|
24
24
|
- **toga2-hub** (TOGa Hub) — 2 doc(s) → [2.0/apps/toga2-hub/INDEX.md](2.0/apps/toga2-hub/INDEX.md)
|
|
25
25
|
- **talos** (TOGa IQ) — 6 doc(s) → [2.0/apps/talos/INDEX.md](2.0/apps/talos/INDEX.md)
|
|
26
|
+
- **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
|
|
26
27
|
|
|
27
28
|
## standalone framework
|
|
28
29
|
|
|
@@ -34,6 +35,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
34
35
|
- **Compass USA** (`compass-usa`) → [clients/compass-usa/INDEX.md](clients/compass-usa/INDEX.md)
|
|
35
36
|
- **Elite** (`elite`) → [clients/elite/INDEX.md](clients/elite/INDEX.md)
|
|
36
37
|
- **New York City Department of Education** (`nycdoe`) → [clients/nycdoe/INDEX.md](clients/nycdoe/INDEX.md)
|
|
38
|
+
- **Office Depot** (`office-depot`) → [clients/office-depot/INDEX.md](clients/office-depot/INDEX.md)
|
|
37
39
|
- **Prudential Financial** (`prudential`) → [clients/prudential/INDEX.md](clients/prudential/INDEX.md)
|
|
38
40
|
- **Rate** (`rate`) → [clients/rate/INDEX.md](clients/rate/INDEX.md)
|
|
39
41
|
- **Tow Foundation** (`tow-foundation`) → [clients/tow-foundation/INDEX.md](clients/tow-foundation/INDEX.md)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Client: Office Depot `office-depot`
|
|
2
|
+
|
|
3
|
+
| Doc | Framework | Summary | Files |
|
|
4
|
+
|-----|-----------|---------|-------|
|
|
5
|
+
| [ODP Tech Support Agent (Office Depot variant)](features/odp-tech-support.md) | 2.0 | ODP-specific customization of the shared TOGa Voice agent. | voice-to-voice/clients/odp/agent.py, voice-to-voice/clients/odp/agents/tech_support.py, voice-to-voice/clients/odp/prompts.py, voice-to-voice/clients/odp/config.yaml, voice-to-voice/clients/odp/.env.example |
|
|
6
|
+
| [Office Depot](profile.md) | 2.0 | Office Depot is the first deployed tenant of the **TOGa Voice** (`voice-to-voice`) platform. | |
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: ODP Tech Support Agent (Office Depot variant)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
project: TOGa Voice
|
|
5
|
+
client: office-depot
|
|
6
|
+
type: client-feature
|
|
7
|
+
status: active
|
|
8
|
+
updated: 2026-06-16
|
|
9
|
+
owners: [akhokhani]
|
|
10
|
+
files:
|
|
11
|
+
- voice-to-voice/clients/odp/agent.py
|
|
12
|
+
- voice-to-voice/clients/odp/agents/tech_support.py
|
|
13
|
+
- voice-to-voice/clients/odp/prompts.py
|
|
14
|
+
- voice-to-voice/clients/odp/config.yaml
|
|
15
|
+
- voice-to-voice/clients/odp/.env.example
|
|
16
|
+
related:
|
|
17
|
+
- ../profile.md
|
|
18
|
+
- ../../../2.0/apps/voice-to-voice/architecture.md
|
|
19
|
+
- ../../../2.0/apps/voice-to-voice/features/livekit-pipeline.md
|
|
20
|
+
- ../../../2.0/apps/voice-to-voice/features/ticket-api-integration.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
|
|
25
|
+
ODP-specific customization of the shared TOGa Voice agent. The platform's
|
|
26
|
+
LiveKit pipeline, tools, and Ticket API integration are unchanged; what
|
|
27
|
+
changes per client is **the system prompt, the config values, and the
|
|
28
|
+
auth keys**. This file documents what's *different* about the ODP
|
|
29
|
+
deployment.
|
|
30
|
+
|
|
31
|
+
Base feature: `2.0/apps/voice-to-voice/features/livekit-pipeline.md`.
|
|
32
|
+
|
|
33
|
+
## What's ODP-specific
|
|
34
|
+
|
|
35
|
+
### Audience and tone
|
|
36
|
+
|
|
37
|
+
- **Audience:** Office Depot **store associates** doing tech-support work
|
|
38
|
+
on store equipment (POS, network, peripheral hardware). **Not** retail
|
|
39
|
+
customers. The system prompt enforces this — `mark_caller_as_customer()`
|
|
40
|
+
is the dedicated short-circuit for the misroute path.
|
|
41
|
+
- **Persona:** "Talos" — warm, calm, human voice. Three rules dominate
|
|
42
|
+
the prompt (`prompts.py`):
|
|
43
|
+
1. Speak plainly — no JSON, no markdown, no acronyms unless the caller
|
|
44
|
+
uses them first.
|
|
45
|
+
2. One question per turn.
|
|
46
|
+
3. **Spell out numbers and identifiers digit-by-digit** for TTS clarity
|
|
47
|
+
(work orders, store numbers, ticket numbers).
|
|
48
|
+
|
|
49
|
+
### Call flow
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
greeting
|
|
53
|
+
→ collect caller name + store_id + work_order (set_caller_info per field)
|
|
54
|
+
→ detect customer misroute → mark_caller_as_customer → polite close
|
|
55
|
+
→ describe issue
|
|
56
|
+
→ search_knowledge_base (Bedrock KB, top-2)
|
|
57
|
+
→ walk through KB steps; if resolved, end_call
|
|
58
|
+
→ if not resolved or repeat caller:
|
|
59
|
+
repeat? link_existing_ticket(number, notes)
|
|
60
|
+
new? upsert_ticket_context(subject, summary, steps_tried, notes, priority)
|
|
61
|
+
→ end_call(farewell)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Tools (ODP exposes the platform set)
|
|
65
|
+
|
|
66
|
+
`set_caller_info`, `search_knowledge_base`, `upsert_ticket_context`,
|
|
67
|
+
`link_existing_ticket`, `mark_caller_as_customer`, `end_call` — same set
|
|
68
|
+
as `TalosAgent`. No ODP-specific tools today.
|
|
69
|
+
|
|
70
|
+
### config.yaml differences from default
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
client_id: "odp"
|
|
74
|
+
client_name: "Office Depot"
|
|
75
|
+
agent_name: "odp-tech-support"
|
|
76
|
+
s3_upload_prefix: "odp/upload"
|
|
77
|
+
tts_voice: "Matthew" # Polly generative
|
|
78
|
+
llm_max_tokens: 300
|
|
79
|
+
kb_max_results: 2
|
|
80
|
+
timezone: "America/Chicago"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Required env vars (per `.env.example`)
|
|
84
|
+
|
|
85
|
+
- `ODP_API_PUBLIC_KEY`, `ODP_API_SECRET_KEY` — Agilant Ticket API keys
|
|
86
|
+
(the **only** auth into TOGa Desk).
|
|
87
|
+
- `LIVEKIT_URL`, `LIVEKIT_API_KEY`, `LIVEKIT_API_SECRET`.
|
|
88
|
+
- `AWS_REGION=us-east-1`, plus IAM role / keys for Transcribe + Bedrock +
|
|
89
|
+
Polly + S3.
|
|
90
|
+
- `BEDROCK_KNOWLEDGE_BASE_ID` — ODP's KB.
|
|
91
|
+
- `S3_BUCKET=talos-voice-to-voice`.
|
|
92
|
+
|
|
93
|
+
Values live only in LiveKit Cloud secrets (synced by `deploy.sh`) — never
|
|
94
|
+
in the repo.
|
|
95
|
+
|
|
96
|
+
### LiveKit Cloud identity
|
|
97
|
+
|
|
98
|
+
```toml
|
|
99
|
+
# clients/odp/livekit.toml
|
|
100
|
+
[project]
|
|
101
|
+
subdomain = "talos-xwwues9e"
|
|
102
|
+
[agent]
|
|
103
|
+
id = "CA_Do8Vhz3oqYH7"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Bedrock Knowledge Base
|
|
107
|
+
|
|
108
|
+
ODP-specific. Documents uploaded to
|
|
109
|
+
`s3://talos-voice-to-voice/togaiq/<category>/odp/archived/`, ingested by
|
|
110
|
+
the ODP KB. The agent's `search_knowledge_base` tool calls this KB only.
|
|
111
|
+
|
|
112
|
+
## How it relates to TOGa Desk 1.0
|
|
113
|
+
|
|
114
|
+
| Touchpoint | Path |
|
|
115
|
+
|---|---|
|
|
116
|
+
| Repeat caller lookup | `GET /ticket/callbacknumber/{digits}` on session start |
|
|
117
|
+
| Work-order validation | `GET /ticket/workorder/{wo}` mid-conversation |
|
|
118
|
+
| Ticket creation | `POST /ticket` from worker shutdown (with `ensure_ticket_created_for_call` guarantee) |
|
|
119
|
+
| Transcript + audio attachment | `PUT /ticket/{number}` from the Lambda post-call processor |
|
|
120
|
+
|
|
121
|
+
Tickets land in `TOGaDeskSupport.tickets` (and related attachment tables)
|
|
122
|
+
via the Agilant API adapter. ODP-specific fields on the Desk side:
|
|
123
|
+
`store_id`, `workorder` (custom columns on the Desk ticket form).
|
|
124
|
+
|
|
125
|
+
See `2.0/apps/voice-to-voice/features/ticket-api-integration.md` for the
|
|
126
|
+
full contract, HMAC scheme, and PII considerations.
|
|
127
|
+
|
|
128
|
+
## Gotchas / known issues
|
|
129
|
+
|
|
130
|
+
- **Spelling-out rule is in the prompt, not enforced in code.** A prompt
|
|
131
|
+
edit can silently break it. After any prompt change, dry-run a call
|
|
132
|
+
with a 7-digit work order and confirm Polly says digit-by-digit.
|
|
133
|
+
- **KB results are capped at 2.** Increasing this hurts latency and
|
|
134
|
+
rarely helps relevance — Bedrock retrieval already top-K's by
|
|
135
|
+
relevance.
|
|
136
|
+
- **`mark_caller_as_customer` must short-circuit cleanly** — do not
|
|
137
|
+
trigger ticket creation on the misroute path; that's noise in Desk
|
|
138
|
+
triage queues.
|
|
139
|
+
- **Repeat-caller dedup is best-effort.** Don't auto-merge tickets — the
|
|
140
|
+
`link_existing_ticket` tool always requires the LLM to confirm the
|
|
141
|
+
match number with the caller out loud.
|
|
142
|
+
|
|
143
|
+
## Change history
|
|
144
|
+
- 2026-06-16 — Initial ODP tech-support client-feature doc. (akhokhani)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Office Depot
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
project: TOGa Voice
|
|
5
|
+
client: office-depot
|
|
6
|
+
type: profile
|
|
7
|
+
status: active
|
|
8
|
+
updated: 2026-06-16
|
|
9
|
+
owners: [akhokhani]
|
|
10
|
+
files: []
|
|
11
|
+
related:
|
|
12
|
+
- features/odp-tech-support.md
|
|
13
|
+
- ../../2.0/apps/voice-to-voice/architecture.md
|
|
14
|
+
- ../../2.0/apps/voice-to-voice/features/ticket-api-integration.md
|
|
15
|
+
- ../../1.0/apps/togadesk/architecture.md
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Summary
|
|
19
|
+
|
|
20
|
+
Office Depot is the first deployed tenant of the **TOGa Voice**
|
|
21
|
+
(`voice-to-voice`) platform. Today the relationship is **inbound phone
|
|
22
|
+
support for Office Depot store associates** (not retail customers): a
|
|
23
|
+
LiveKit voice agent on a toll-free line answers calls from store staff,
|
|
24
|
+
walks them through tech-support troubleshooting (point of sale, networking,
|
|
25
|
+
peripheral hardware), and creates / updates tickets in **TOGa Desk 1.0**
|
|
26
|
+
via the Agilant Ticket API.
|
|
27
|
+
|
|
28
|
+
Office Depot also appears as a **vendor** of the Compass USA TOGa client
|
|
29
|
+
(unrelated to this voice deployment — different team, different system).
|
|
30
|
+
This profile only covers the **voice / Desk** relationship.
|
|
31
|
+
|
|
32
|
+
## Platforms & data
|
|
33
|
+
|
|
34
|
+
- **TOGa Voice (2.0 — Python / LiveKit):** ODP agent at
|
|
35
|
+
`voice-to-voice/clients/odp/`. Deployed to LiveKit Cloud as agent
|
|
36
|
+
`CA_Do8Vhz3oqYH7` under project subdomain `talos-xwwues9e`.
|
|
37
|
+
- **TOGa Desk 1.0 (PHP):** the system of record for tickets. Voice writes
|
|
38
|
+
reach Desk via the Agilant Ticket API at `api.agilantsolutions.com` —
|
|
39
|
+
never directly via the PHP `/desk/api/`.
|
|
40
|
+
- **AWS:** shared `talos-voice-to-voice` S3 bucket, client prefix `odp/`.
|
|
41
|
+
Bedrock Knowledge Base per ODP (id in env). Lambda post-processor scans
|
|
42
|
+
`odp/upload/`.
|
|
43
|
+
|
|
44
|
+
## Voice configuration (ODP-specific)
|
|
45
|
+
|
|
46
|
+
| Field | Value |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `client_id` | `odp` |
|
|
49
|
+
| `client_name` | `Office Depot` |
|
|
50
|
+
| `agent_name` | `odp-tech-support` |
|
|
51
|
+
| TTS voice | Amazon Polly **Matthew** (generative) |
|
|
52
|
+
| Timezone | `America/Chicago` |
|
|
53
|
+
| KB max results | 2 |
|
|
54
|
+
| LLM max tokens | 300 |
|
|
55
|
+
| S3 upload prefix | `odp/upload` |
|
|
56
|
+
|
|
57
|
+
System prompt is in `clients/odp/prompts.py`; persona is "Talos" — warm,
|
|
58
|
+
calm, human-sounding tech-support agent for store associates.
|
|
59
|
+
|
|
60
|
+
## Call flow (one-paragraph)
|
|
61
|
+
|
|
62
|
+
Store associate dials the ODP toll-free number → LiveKit SIP routes to the
|
|
63
|
+
ODP agent worker → background `fetch_recent_tickets(callback_number)` warms
|
|
64
|
+
repeat-caller context → agent greets, asks for name + store + work order
|
|
65
|
+
→ branches: misrouted retail customer → polite close; new issue → KB
|
|
66
|
+
search + troubleshooting + ticket creation; repeat caller → link existing
|
|
67
|
+
ticket → goodbye. End-of-call → S3 ticket folder + `session_report.json`
|
|
68
|
+
written → Lambda picks up, builds TXT report, PUTs to the ticket.
|
|
69
|
+
|
|
70
|
+
## Key features (this client)
|
|
71
|
+
|
|
72
|
+
- [ODP Tech Support Agent](features/odp-tech-support.md) — ODP-specific
|
|
73
|
+
prompt / tool surface / config, layered over the shared platform.
|
|
74
|
+
|
|
75
|
+
## Gotchas / known issues
|
|
76
|
+
|
|
77
|
+
- **Audience is store staff, not retail customers.** When the LLM
|
|
78
|
+
misclassifies a retail caller (the misroute path), the
|
|
79
|
+
`mark_caller_as_customer()` tool short-circuits the flow — keep that
|
|
80
|
+
branch.
|
|
81
|
+
- **Phone identification is best-effort.** A shared store landline can
|
|
82
|
+
yield false-positive "repeat caller" matches; the dedup scorer in
|
|
83
|
+
`core/services/ticket_api.py` produces an `exact|store_only|phone_only`
|
|
84
|
+
rank, never silently merges.
|
|
85
|
+
- **PII at rest.** Transcripts + audio sit in S3 with associate names,
|
|
86
|
+
phone numbers, store IDs. Treat the `odp/` prefix as PII-class.
|
|
87
|
+
|
|
88
|
+
## Change history
|
|
89
|
+
- 2026-06-16 — Initial Office Depot profile — first voice tenant, Desk 1.0 ticketing. (akhokhani)
|
package/knowledge/registry.json
CHANGED
|
@@ -15,5 +15,6 @@
|
|
|
15
15
|
{ "repo": "webhook", "project": "Webhook", "framework": "1.0", "role": "app", "dependsOn": ["library"] },
|
|
16
16
|
{ "repo": "walmarttechservices", "project": "Walmart Tech Services", "framework": "1.0", "role": "app", "dependsOn": ["library"] },
|
|
17
17
|
{ "repo": "talos", "project": "TOGa IQ", "framework": "2.0", "role": "app", "dependsOn": [] },
|
|
18
|
-
{ "repo": "test", "project": "Test", "framework": "1.0", "role": "app", "dependsOn": ["library"] }
|
|
18
|
+
{ "repo": "test", "project": "Test", "framework": "1.0", "role": "app", "dependsOn": ["library"] },
|
|
19
|
+
{ "repo": "voice-to-voice", "project": "TOGa Voice", "framework": "2.0", "role": "app", "dependsOn": [] }
|
|
19
20
|
]
|
package/package.json
CHANGED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Python Rules (applies to all TOGA Python repos)
|
|
2
|
+
|
|
3
|
+
Applies to every TOGA Python project: **talos** (`aegra-api`, `react_agent`,
|
|
4
|
+
`mcp-servers/*`) and **voice-to-voice** (`core/`, `clients/*`,
|
|
5
|
+
`lambda_updated/`). Future Python repos inherit these by default.
|
|
6
|
+
|
|
7
|
+
## Toolchain
|
|
8
|
+
|
|
9
|
+
- **Package manager: `uv`** is canonical. Never run `pip install` ad hoc; add
|
|
10
|
+
to `pyproject.toml` and `uv sync`. Never use Poetry.
|
|
11
|
+
- **Build backend: `hatchling`** in `[build-system]`. No setuptools, no flit.
|
|
12
|
+
- **Python ≥ 3.11.** 3.12 features are allowed when the target image
|
|
13
|
+
supports it (voice-to-voice runs 3.12).
|
|
14
|
+
|
|
15
|
+
## Layout
|
|
16
|
+
|
|
17
|
+
- **`src/` layout mandatory** for any installable package. Tests live in
|
|
18
|
+
`tests/` parallel to `src/`.
|
|
19
|
+
- **Module structure inside `src/<pkg>/`** (the aegra-api template):
|
|
20
|
+
- `api/` — FastAPI routers (one file per domain).
|
|
21
|
+
- `services/` — business logic.
|
|
22
|
+
- `models/` — Pydantic schemas, errors.
|
|
23
|
+
- `core/` — foundational infra (auth, db, config, encryption).
|
|
24
|
+
- `middleware/` — ASGI/HTTP middleware.
|
|
25
|
+
- `observability/` — logging, tracing, metrics setup.
|
|
26
|
+
- `utils/` — pure utilities; **no I/O, no globals**.
|
|
27
|
+
- **Flat layout is forbidden** for new packages.
|
|
28
|
+
|
|
29
|
+
## Async
|
|
30
|
+
|
|
31
|
+
- **Async-first.** FastAPI routes and I/O service methods are `async def`.
|
|
32
|
+
Use `httpx` — **never `requests`** in async paths.
|
|
33
|
+
- **Never block the event loop.** Wrap sync libs with `asyncio.to_thread(...)`
|
|
34
|
+
or an executor.
|
|
35
|
+
- **Handle `asyncio.CancelledError` explicitly** in long-running services
|
|
36
|
+
(skip cleanup if the task is being re-enqueued — see
|
|
37
|
+
`services/run_executor.py`).
|
|
38
|
+
|
|
39
|
+
## Typing
|
|
40
|
+
|
|
41
|
+
- **Type hints everywhere.** No untyped public parameters or returns.
|
|
42
|
+
- **`from __future__ import annotations`** at the top of every module.
|
|
43
|
+
- **Pydantic v2 for all DTOs and Settings.** Use `dataclass` only for
|
|
44
|
+
internal value objects, never for request/response shapes.
|
|
45
|
+
- Use `Field(..., description=...)` on every cross-boundary field — the
|
|
46
|
+
description appears in OpenAPI.
|
|
47
|
+
- Cross-field invariants: `@field_validator` and
|
|
48
|
+
`@model_validator(mode="after")` (see `models/runs.py`).
|
|
49
|
+
|
|
50
|
+
## Config
|
|
51
|
+
|
|
52
|
+
- **`pydantic_settings.BaseSettings` is the only config base class.**
|
|
53
|
+
- Precedence: **env vars > `.env.local` > `.env` > YAML defaults > field
|
|
54
|
+
defaults**.
|
|
55
|
+
- Non-secret defaults go in `config.yaml` (voice-to-voice pattern). Secrets
|
|
56
|
+
come from env vars only — never commit `.env` (only `.env.example`).
|
|
57
|
+
- `SettingsConfigDict(env_file=(".env", ".env.local"), extra="ignore")` is
|
|
58
|
+
the standard. Use `extra="ignore"`, not `"forbid"`, so future-added env
|
|
59
|
+
vars don't break old deploys.
|
|
60
|
+
- **Multiple Settings subclasses per module are fine** (aegra-api has 13).
|
|
61
|
+
Do not collapse them into one mega-class.
|
|
62
|
+
|
|
63
|
+
## Logging
|
|
64
|
+
|
|
65
|
+
- **`structlog` for all operational logs.** Module-level
|
|
66
|
+
`logger = structlog.get_logger(__name__)`.
|
|
67
|
+
- **Structured fields, not f-strings.**
|
|
68
|
+
`logger.info("ticket.created", ticket_id=tid)` — never
|
|
69
|
+
`logger.info(f"created ticket {tid}")`.
|
|
70
|
+
- **Correlation IDs via `asgi-correlation-id`** middleware on every HTTP
|
|
71
|
+
service.
|
|
72
|
+
- Stdlib `logging` is allowed only at startup, before structlog is
|
|
73
|
+
configured.
|
|
74
|
+
|
|
75
|
+
## Errors
|
|
76
|
+
|
|
77
|
+
- **No bare `except Exception:`.** Catch the narrowest exception that fits.
|
|
78
|
+
- Custom exception types live in `models/errors.py` (or `<pkg>/errors.py`).
|
|
79
|
+
- **FastAPI:** raise `HTTPException(status_code, detail=...)` at the router
|
|
80
|
+
layer. Services raise domain exceptions; an exception handler in `main.py`
|
|
81
|
+
maps them.
|
|
82
|
+
- **Retry only on transient (5xx, connection) errors.** 4xx fails fast.
|
|
83
|
+
See `core/services/ticket_api.py` and `lambda_updated/api_client.py`.
|
|
84
|
+
|
|
85
|
+
## HTTP clients
|
|
86
|
+
|
|
87
|
+
- **`httpx.AsyncClient` for async, `requests` only in sync scripts.**
|
|
88
|
+
- Always pass `timeout=` explicitly — never accept the default.
|
|
89
|
+
- Centralize auth/signing in one client module per external service
|
|
90
|
+
(e.g. `ticket_api.py` for the Agilant Ticket API). Routes call the client,
|
|
91
|
+
never `httpx` directly.
|
|
92
|
+
|
|
93
|
+
## Migrations
|
|
94
|
+
|
|
95
|
+
- **Alembic** for any schema touch. Migration files
|
|
96
|
+
`YYYYMMDDHHMMSS_description.py` under `alembic/versions/`.
|
|
97
|
+
- Use `op.create_table(...)` / `op.add_column(...)` — never raw
|
|
98
|
+
`CREATE TABLE` strings in migrations.
|
|
99
|
+
- **Always implement `downgrade()`**, even if it's an explicit
|
|
100
|
+
`op.execute("...")` reverse. Empty downgrades break rollback.
|
|
101
|
+
- PostgreSQL extensions are enabled in migrations
|
|
102
|
+
(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`), not at runtime.
|
|
103
|
+
|
|
104
|
+
## Tests
|
|
105
|
+
|
|
106
|
+
- **`pytest` only** — no `unittest.TestCase`.
|
|
107
|
+
- Async tests: `asyncio_mode = "auto"` in `pyproject.toml`; plain
|
|
108
|
+
`async def test_*` works without decoration.
|
|
109
|
+
- Test layout: `tests/unit/`, `tests/integration/`. Fixtures in
|
|
110
|
+
`conftest.py` per directory.
|
|
111
|
+
- Marker set: `unit` (no external calls), `api` (real HTTP, no LLM),
|
|
112
|
+
`integration` (full LLM + API). Declare any new marker in
|
|
113
|
+
`[tool.pytest.ini_options].markers`.
|
|
114
|
+
|
|
115
|
+
## Secrets and PII
|
|
116
|
+
|
|
117
|
+
- **Never paste a credential value in code or in committed docs.** Reference
|
|
118
|
+
the env var name (e.g. `ODP_API_SECRET_KEY`). The `capture` publisher
|
|
119
|
+
scans for credential literals and fails the publish if any are found.
|
|
120
|
+
- **Use `cryptography.Fernet`** (symmetric) for at-rest encryption of
|
|
121
|
+
metadata; helper lives at `core/encryption.py` in aegra-api.
|
|
122
|
+
- **AWS SigV4 / Authorization headers must be redacted in logs.** See
|
|
123
|
+
`RedactSigV4` in voice-to-voice-new for the helper.
|
|
124
|
+
- Validate metadata keys against a whitelist regex before persisting them
|
|
125
|
+
(see `api/threads.py:49`).
|
|
126
|
+
- `bandit` runs as a dev dep; fix findings, don't suppress them.
|
|
127
|
+
|
|
128
|
+
## Don't
|
|
129
|
+
|
|
130
|
+
- `pip install` inside the repo. Use `uv add`.
|
|
131
|
+
- `requests` in async code.
|
|
132
|
+
- f-strings inside `logger.*` calls.
|
|
133
|
+
- Bare `except:` or `except Exception:` without a re-raise.
|
|
134
|
+
- Hardcoded URLs, hosts, or credentials anywhere in source.
|
|
135
|
+
- A new "utils.py" grab bag — place pure helpers in `utils/<topic>.py`.
|
|
136
|
+
- Mixing sync and async inside the same service class.
|