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.
@@ -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)
@@ -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)
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.91",
3
+ "version": "1.0.92",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",
@@ -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.