Skip to content

Commit 8b5566f

Browse files
committed
feat(pour-completed): add route for pour completed events and Redis publishing
1 parent 0c80495 commit 8b5566f

3 files changed

Lines changed: 89 additions & 0 deletions

File tree

apps/server/src/app.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { createV1CompletionsRoutes } from './routes/openai/v1'
5050
import { createProviderRoutes } from './routes/providers'
5151
import { createStripeRoutes } from './routes/stripe'
5252
import { createSystemMessageEventsRoute } from './routes/events/system-messages'
53+
import { createPourCompletedRoutes } from './routes/pour-completed'
5354
import { createSystemMessageRoutes } from './routes/system-message'
5455
import { createBillingMq } from './services/billing/billing-events'
5556
import { createBillingService } from './services/billing/billing-service'
@@ -233,6 +234,13 @@ export async function buildApp(deps: AppDeps) {
233234
*/
234235
.route('/api/events/system-messages', createSystemMessageEventsRoute(deps.redis))
235236

237+
/**
238+
* Pour-completed signal from main_task_node. Republished to the
239+
* `robot:pour-completed` Redis channel so bt_supervisor's PostPour branch
240+
* can drive the follow-up question + go_home timeout.
241+
*/
242+
.route('/api/robot/pour-completed', createPourCompletedRoutes(deps.redis))
243+
236244
/**
237245
* V1 routes for official provider.
238246
*/

apps/server/src/routes/chat-ws/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,22 @@ export function createChatWsHandlers(
4545
const result = await chatService.pushMessages(userId, req!.chatId, req!.messages)
4646
const payload = await deliverPushedMessages(chatService, redis, userId, req!.chatId, result, ctx)
4747
metrics?.wsMessagesSent.add(payload.messages.length)
48+
49+
// Fan out user-role messages to ROS-side consumers (bt_supervisor) over
50+
// Redis. Single-purpose channel: any subscriber treats this as "the
51+
// human just said something." Used by the post-pour question wait.
52+
// Best-effort; never blocks the RPC reply.
53+
for (const m of req!.messages) {
54+
if (m.role !== 'user')
55+
continue
56+
redis.publish('robot:user-response', JSON.stringify({
57+
text: m.content,
58+
userId,
59+
chatId: req!.chatId,
60+
at: new Date().toISOString(),
61+
})).catch(err => log.withFields({ err }).warn('robot:user-response publish failed'))
62+
}
63+
4864
return { seq: result.seq }
4965
})
5066

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type Redis from 'ioredis'
2+
3+
import type { HonoEnv } from '../types/hono'
4+
5+
import { useLogger } from '@guiiai/logg'
6+
import { Hono } from 'hono'
7+
import { object, optional, safeParse, string } from 'valibot'
8+
9+
import { createBadRequestError } from '../utils/error'
10+
11+
const logger = useLogger('pour-completed')
12+
13+
const POUR_COMPLETED_CHANNEL = 'robot:pour-completed'
14+
15+
/**
16+
* Inbound payload for `POST /api/robot/pour-completed`.
17+
*
18+
* Posted by main_task_node when an `ExecuteDrinkTask` action returns success
19+
* (excluding the `__go_home__` cleanup goal). Republished verbatim to the
20+
* `robot:pour-completed` Redis channel so the BT supervisor can drive the
21+
* post-pour follow-up branch.
22+
*/
23+
const PourCompletedSchema = object({
24+
drink_type: string(),
25+
table_id: optional(string()),
26+
})
27+
28+
/**
29+
* Build the `/api/robot/pour-completed` Hono sub-app.
30+
*
31+
* Use when:
32+
* - Wiring routes in `app.ts`. Mount under `/api/robot/pour-completed`.
33+
*
34+
* Expects:
35+
* - `redis` is the primary connected client; reused for the publish.
36+
*
37+
* Returns:
38+
* - A Hono sub-app exposing `POST /`.
39+
*/
40+
export function createPourCompletedRoutes(redis: Redis) {
41+
return new Hono<HonoEnv>()
42+
.post('/', async (c) => {
43+
const raw = await c.req.json().catch(() => null)
44+
if (raw === null)
45+
throw createBadRequestError('Body must be JSON', 'INVALID_REQUEST')
46+
47+
const parsed = safeParse(PourCompletedSchema, raw)
48+
if (!parsed.success)
49+
throw createBadRequestError('Invalid Request', 'INVALID_REQUEST', parsed.issues)
50+
51+
const payload = JSON.stringify({
52+
drink_type: parsed.output.drink_type,
53+
table_id: parsed.output.table_id ?? '',
54+
at: new Date().toISOString(),
55+
})
56+
57+
redis.publish(POUR_COMPLETED_CHANNEL, payload).catch((err) => {
58+
logger.withError(err as Error).warn('Failed to publish robot:pour-completed')
59+
})
60+
61+
logger.withFields({ ...parsed.output }).log('Pour completed event published')
62+
63+
return c.json({ success: true }, 201)
64+
})
65+
}

0 commit comments

Comments
 (0)