feat(vocero): add linked content and TALKFIRST greetings (#2)

Opening a report or podcast link now establishes an authenticated starting reference for the conversation. Users can select other accessible content or compare reports while retaining the original link, and TALKFIRST greetings can name the verified report or podcast.

Release notes:

- Added authenticated report and episode link resolution with bounded lookups and unavailable-content handling
- Added explicit conversation selection without restricting searches or changing the reference during comparisons
- Added contextual TALKFIRST greetings through both voice-proxy and HTTP reasoning paths
- Preserved backend permission checks, conversation isolation, cancellation, and the existing speech protocol

Reviewed-on: AquaPortal/s2s#2
This commit is contained in:
2026-10-08 09:34:16 -04:00
parent 96895996d9
commit 4b0108ecf0
21 changed files with 946 additions and 66 deletions
+43
View File
@@ -0,0 +1,43 @@
"""Compact, immutable references returned by authenticated content lookups."""
from datetime import date
from typing import Annotated, Literal
from pydantic import BaseModel, ConfigDict, Field, StringConstraints
ContentId = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=200)]
PodcastId = Annotated[str, StringConstraints(pattern=r"^(daily|report):\S.{0,199}$")]
class ContentReference(BaseModel):
model_config = ConfigDict(frozen=True)
id: ContentId
title: str = Field(min_length=1, max_length=2000)
date: date
class ReportReference(ContentReference):
has_podcast: bool
class PodcastReference(ContentReference):
id: PodcastId
kind: Literal["daily", "report"]
class ConversationContent(BaseModel):
model_config = ConfigDict(frozen=True)
report: ReportReference | None = None
podcast: PodcastReference | None = None
unavailable: bool = False
class ContentSelection(BaseModel):
model_config = ConfigDict(extra="forbid")
kind: Literal["report", "podcast"]
id: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=207)]
class SelectContentRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
request: ContentSelection