refined-airport-68974
12/29/2025, 6:04 AMtall-airline-16756
12/29/2025, 6:05 AMtall-airline-16756
12/29/2025, 6:05 AMagentName to be stored on the participant token (so the client can read it) — e.g., in metadata or attributes — OR do you want this token to trigger an automatic agent dispatch when the participant joins (room-level agent_name)?
• Which SDK/dependency are you using to generate tokens in your Spring Boot app (plain JWT library, LiveKit server-sdk-kotlin, or something else)?
• Please paste the current token-generation code snippet you have (or share the relevant Maven/Gradle dependency).
Once you tell me the answers I’ll provide a concise Java example you can drop into your Spring Boot service.
Sources: server-sdk-kotlin | livekit/livekit_composite | DeepWiki | server-sdk-go | livekit/livekit_composite | DeepWikirefined-airport-68974
12/29/2025, 6:06 AMagent_name)tall-airline-16756
12/29/2025, 6:06 AMagent_name into the token.
1) Add the SDK dependency (Maven):
<dependency>
<groupId>io.livekit</groupId>
<artifactId>server-sdk-kotlin</artifactId>
<version>1.4.0</version>
</dependency>
2) Token-generation method (Java):
import io.livekit.server.AccessToken;
import io.livekit.server.RoomConfiguration;
import io.livekit.server.RoomAgentDispatch;
import io.livekit.server.VideoGrant;
import java.util.Optional;
public String generateTokenWithAgentDispatch(
String apiKey,
String apiSecret,
String identity,
String roomName,
String agentName,
String metadataJson // optional JSON string passed to the agent as job metadata
) {
RoomAgentDispatch agentDispatch = RoomAgentDispatch.newBuilder()
.setAgentName(agentName)
.setMetadata(Optional.ofNullable(metadataJson).orElse(""))
.build();
RoomConfiguration roomConfig = RoomConfiguration.newBuilder()
.addAgents(agentDispatch)
.build();
AccessToken token = new AccessToken(apiKey, apiSecret)
.setIdentity(identity)
.setName("User")
.addGrants(new VideoGrant.Builder()
.roomJoin(true)
.room(roomName)
.build())
.setRoomConfig(roomConfig);
return token.toJwt();
}
Notes:
• The agentName in RoomAgentDispatch causes that specific agent to be explicitly dispatched to the room when the token is used.
• metadataJson is optional and becomes the agent job metadata (accessible to the agent).
• Automatic dispatch (assigning an agent to all rooms) is different from explicit dispatch via token; this code performs explicit dispatch.
If you’d like, share your current token code or your preferred JSON structure for metadataJson and I’ll adapt the snippet to match.
Sources: server-sdk-kotlin | livekit/livekit_composite | DeepWiki | Agent dispatch | LiveKit docsrefined-airport-68974
12/29/2025, 6:14 AMtall-airline-16756
12/29/2025, 6:14 AM<dependency>
<groupId>io.livekit</groupId>
<artifactId>server-sdk-kotlin</artifactId>
<version>1.4.0</version>
</dependency>
Gradle (Kotlin DSL):
implementation("io.livekit:server-sdk-kotlin:1.4.0")
Would you like me to update the token-generation snippet to exactly match your build (Maven or Gradle) or your current project setup?
Sources: server-sdk-kotlin | livekit/livekit_composite | DeepWikirefined-airport-68974
12/29/2025, 6:31 AMtall-airline-16756
12/29/2025, 6:31 AM<dependency>
<groupId>io.livekit</groupId>
<artifactId>server-sdk-kotlin</artifactId>
<version>1.4.0</version>
</dependency>
After Maven refresh, you can use the Java token-generation snippet I provided earlier (it works with this JVM SDK). Would you like the full pom.xml snippet or a ready-to-drop Spring Boot service method that generates the token?refined-airport-68974
12/29/2025, 6:32 AMtall-airline-16756
12/29/2025, 6:32 AMio.livekit:server-sdk-kotlin:1.4.0. That artifact is not published to the public Maven repositories you have configured, so Maven will fail to resolve it.
You have three practical options — pick one and I’ll give the exact steps:
• Build & install the JVM SDK locally
a. Clone the SDK repo: git clone <https://github.com/livekit/server-sdk-kotlin.git>
b. From the repo root run: ./gradlew publishToMavenLocal
c. Keep the same dependency in your pom; Maven will resolve it from your local Maven repo.
• Use a different supported server SDK that is available publicly (immediate unblock)
◦ For example, generate tokens with the Node or Go server SDK (both have published packages). I can provide a minimal Node/Go token-generator you can run as a small service.
• Verify alternate artifact coordinates or a published version
◦ I can search the exact groupId/artifactId/version if you want me to confirm the correct Maven coordinates. If the SDK was published under a different coordinate or version, I’ll provide the exact pom entry.
Which option would you like? If you choose "build & install locally," tell me whether you have Gradle installed or want the exact commands to run. If you prefer an alternative SDK, tell me which language (Node or Go).
Sources: server-sdk-kotlin | livekit/livekit_composite | DeepWikirefined-airport-68974
12/29/2025, 6:35 AM<dependency>
<groupId>io.livekit</groupId>
<artifactId>livekit-server</artifactId>
<version>0.10.1</version>
</dependency>tall-airline-16756
12/29/2025, 6:35 AMio.livekit:livekit-server:0.10.1 artifact does NOT support embedding a room-level agent_name (agent dispatch) in tokens.
You have three practical paths:
1) Build & use the JVM server SDK locally (closest to your Java flow)
• Clone the server SDK repo: git clone <https://github.com/livekit/server-sdk-kotlin.git>
• From the repo root run: ./gradlew publishToMavenLocal
• Add the locally published dependency to your pom (use the version printed by the build):
<dependency>
<groupId>io.livekit</groupId>
<artifactId>server-sdk-kotlin</artifactId>
<version>{version-from-build}</version>
</dependency>
I can give exact Gradle commands and the expected version string if you want.
2) Keep using Maven + your Spring Boot app but generate dispatching tokens from another service
• Run a small token-generator service using a published SDK (Node or Go) that supports room config/agent dispatch.
• Your Spring app calls that internal service to get tokens. I can provide a minimal Node or Go example.
3) Trigger dispatch via the Agent Dispatch API instead of token claims
• After creating/joining the room, call the agent-dispatch API to ask a named agent to join (requires a server SDK or direct HTTP call to the LiveKit control API). I can show the API request pattern.
Which option do you want to pursue? If you choose 1, tell me whether you can run Gradle locally and I’ll give exact commands and the pom snippet to use. If you prefer 2, tell me Node or Go and I’ll provide a ready token-generator.
Sources: server-sdk-kotlin | livekit/livekit_composite | DeepWiki | server-sdk-js | livekit/livekit_composite | DeepWiki | Agent dispatch | LiveKit docsrefined-airport-68974
12/29/2025, 6:43 AMtall-airline-16756
12/29/2025, 6:43 AMgit clone <https://github.com/livekit/server-sdk-kotlin.git>
b. From the repo root run: ./gradlew publishToMavenLocal
c. Add the published artifact to your pom (use the version printed by the build).
I can give exact Gradle commands and the pom snippet.
• Use the Agent Dispatch API (no SDK build)
◦ After room creation or participant join, call the agent-dispatch endpoint to tell a named agent to join. This works with any HTTP client from your Spring app. I can provide the exact HTTP request/Java snippet.
• Run a small token-generator service using a published SDK (Node or Go)
◦ Create a tiny service (Node or Go) that returns tokens with RoomConfiguration/agents and call it from your Spring app. I can provide a ready Node or Go implementation.
Which option do you want to pursue? If you choose "build & use SDK locally," confirm you can run Gradle locally and I’ll give exact commands and pom entries. If you choose API or Node/Go, tell me which and I’ll send the code.
Sources: server-sdk-kotlin | livekit/livekit_composite | DeepWiki | Agent dispatch | LiveKit docsrefined-airport-68974
12/29/2025, 6:45 AMtall-airline-16756
12/29/2025, 6:45 AMsupport- or sales-.
• Token-based conditional dispatch
When you generate a token, embed a RoomConfiguration with agent dispatch only for rooms of a certain type. Create tokens for “support” rooms that include the agent dispatch, and omit it for other rooms.
• Programmatic dispatch via API
After a room is created or a participant joins, call the Agent Dispatch API from your backend to dispatch an agent only if the room meets your criteria (name, metadata, user role, etc.).
• Agent-side filtering (early exit)
Let the agent receive dispatches but immediately check room attributes (name, metadata) in its entrypoint and exit if the room doesn’t match. This is simple and keeps dispatch logic centralized in the agent.
Which approach do you prefer (server-side token generation, API-driven dispatch, naming-rule dispatch, or agent-side filtering)? Tell me your current flow (how rooms are created/named and where tokens are generated) and I’ll give a focused example for that option.
Sources: Agents telephony integration | LiveKit docs | Agent dispatch | LiveKit docs | server-sdk-go | livekit/livekit_composite | DeepWikirefined-airport-68974
12/29/2025, 6:46 AMtall-airline-16756
12/29/2025, 6:47 AMsupport-), include a RoomConfiguration with the agent dispatch; otherwise skip it.
• This avoids extra API calls and only dispatches when the token is created for that room.
Java pseudocode (use your token generator):
if (roomName.startsWith("support-")) {
RoomAgentDispatch agent = RoomAgentDispatch.newBuilder()
.setAgentName("support-agent")
.setMetadata("{\"source\":\"token\"}")
.build();
RoomConfiguration cfg = RoomConfiguration.newBuilder().addAgents(agent).build();
token.setRoomConfig(cfg);
}
(Use your server SDK or the approach you can build locally — same conditional logic applies.)
2) Central dispatch rules (if using SIP/dispatch configuration)
• Configure dispatch rules that match room prefixes and attach roomConfig for those rules.
• Example rule (conceptual JSON):
{
"dispatch_rule": {
"rule": {
"roomPrefix": "support-"
},
"roomConfig": {
"agents": [
{
"agentName": "support-agent",
"metadata": "{\"role\":\"support\"}"
}
]
}
}
}
• This makes any room created with support-... automatically receive the configured agent.
3) API-driven dispatch after room creation
• If you prefer not to modify tokens, have your backend evaluate new rooms and call the Agent Dispatch API when the room name matches:
if (roomName.startsWith("support-")) {
// HTTP or SDK call to agent dispatch endpoint:
// { "room": roomName, "agent_name": "support-agent", "metadata": "..." }
}
• Works well if rooms are created separately from token issuance.
4) Agent-side defensive check (recommended as a safety net)
• Even with rules above, have the agent check room name on startup and exit if it doesn’t match expected patterns:
if not ctx.room.name.startswith("support-"):
await ctx.shutdown()
• Prevents accidental joins if dispatch rules are misapplied.
Which of these do you want to implement now?
• I can give a concrete Java snippet for your token generator (Maven/Spring) that conditionally sets RoomConfiguration,
• or provide the exact HTTP request body to call Agent Dispatch from Java,
• or an example dispatch-rule JSON you can use where you configure dispatch rules.refined-airport-68974
12/29/2025, 6:51 AMtall-airline-16756
12/29/2025, 6:51 AMio.livekit:livekit-server:0.10.1 dependency and implement Option 1 (conditionally embed agent dispatch in tokens by room name).
Short answer: You cannot do Option 1 with io.livekit:livekit-server:0.10.1. That artifact does not provide the RoomConfiguration / RoomAgentDispatch features required to embed agent dispatch into tokens.
What you can do next (pick one) — I can provide exact steps/snippets for any option:
• Build & use the JVM server SDK locally (enables Option 1)
◦ Clone the server SDK: git clone <https://github.com/livekit/server-sdk-kotlin.git>
◦ From the repo root run: ./gradlew publishToMavenLocal
◦ Add the published server-sdk-kotlin dependency to your pom and use the token code that conditionally sets RoomConfiguration when roomName matches your pattern.
◦ I can provide the precise Gradle commands, pom entry, and a ready-to-drop Java method that does the conditional token generation.
• Keep livekit-server and use API-driven dispatch instead (works with your current dependency)
◦ After room creation or participant join, call the Agent Dispatch API from your Spring app when roomName matches (I can give the exact Java HTTP request body/headers).
◦ This achieves the same outcome (agent only joins support-* rooms) without changing dependencies.
• Add a small helper service using a published SDK (Node/Go) to create tokens with room configs
◦ Your Spring app calls this service for tokens when roomName matches; otherwise it issues regular tokens.
• As a safety net, have agents check room name on startup and exit if it doesn't match (I can provide the agent-side snippet).
Which path do you want to take? If you want Option 1, confirm you can run Gradle locally and I’ll give the exact commands, pom snippet, and full Java token-generator method that conditionally embeds the agent dispatch. If you prefer to avoid changing dependencies, I’ll provide the API-based dispatch Java snippet.refined-airport-68974
12/29/2025, 6:52 AMtall-airline-16756
12/29/2025, 6:52 AMString dispatchUrl = "https://<your-livekit-control-endpoint>/v1/agent/dispatch";
String body = """
{
"room":"support-123",
"agent_name":"support-agent",
"metadata":"{\"source\":\"post-join\",\"userId\":\"u123\"}"
}
""";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(dispatchUrl))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + YOUR_SERVER_JWT) // use server auth token
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpClient client = HttpClient.newHttpClient();
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
// check resp.statusCode() for success
How to trigger this in your app:
• If you control token issuance: call dispatch right after issuing a token for a matching room.
• If you want to ensure a join happened: use LiveKit webhooks (participant_joined) or server SDK events to detect joins, then call dispatch.
• If you only need the agent when the first participant arrives, call dispatch on the first join event and ignore subsequent joins.
Would you like the exact HTTP endpoint and auth pattern for your LiveKit deployment (self-hosted vs cloud) and a Spring RestTemplate example wired into a join webhook handler?refined-airport-68974
12/29/2025, 6:53 AMtall-airline-16756
12/29/2025, 6:54 AM// 1) generate token and return it to client (your existing code)
// String jwt = generateToken(...);
// return jwt to client
// 2) then call dispatch
String dispatchUrl = "https://<your-livekit-control-endpoint>/v1/agent/dispatch";
String body = String.format("""
{
"room":"%s",
"agent_name":"support-agent",
"metadata":"%s"
}
""", roomName, "{\"source\":\"post-token\",\"userId\":\"u123\"}");
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(dispatchUrl))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + YOUR_SERVER_JWT)
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpClient client = HttpClient.newHttpClient();
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
// check resp.statusCode() and handle errors/retries as needed
Recommendations:
• If you want agent present as soon as possible: dispatch immediately after returning token.
• If you must ensure a participant actually joined: listen for participant_joined webhook/event and dispatch on the first join.
• Add retry/backoff and idempotency checks for robustness.
Do you want a Spring RestTemplate or WebClient version of the dispatch call and a short example of wiring it into your token-issuance endpoint?refined-airport-68974
12/29/2025, 6:55 AMparticipant_joined webhook/eventtall-airline-16756
12/29/2025, 6:55 AMevent or type == participant_joined (or check payload fields).
Minimal Spring Boot controller (Java 11+):
@RestController
@RequestMapping("/webhook/livekit")
public class LivekitWebhookController {
private final String expectedSecret = "YOUR_WEBHOOK_SECRET"; // config
@PostMapping
public ResponseEntity<String> handleWebhook(@RequestHeader Map<String,String> headers,
@RequestBody String body) {
// 1) verify signature (example HMAC-SHA256 header "X-Signature")
String sig = headers.getOrDefault("X-Signature", "");
if (!verifyHmacSha256(body, sig, expectedSecret)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("invalid signature");
}
// 2) parse event
JSONObject json = new JSONObject(body);
String event = json.optString("event", json.optString("type", ""));
if ("participant_joined".equals(event) || "participant.joined".equals(event)) {
String room = json.getJSONObject("participant").optString("room");
String participantSid = json.getJSONObject("participant").optString("sid");
// 3) conditional dispatch
if (room != null && room.startsWith("support-")) {
dispatchAgentToRoom(room, "support-agent", "{\"source\":\"webhook\",\"participantSid\":\"" + participantSid + "\"}");
}
}
return ResponseEntity.ok("ok");
}
private boolean verifyHmacSha256(String payload, String signature, String secret) {
// implement HMAC-SHA256 of payload with secret and compare to signature
return true;
}
private void dispatchAgentToRoom(String room, String agentName, String metadata) {
// call Agent Dispatch API (same pattern as earlier example)
}
}
3) Dispatch call
• From the webhook handler call your Agent Dispatch endpoint (HTTP POST) with JSON:
{ "room": "support-123", "agent_name":"support-agent", "metadata":"{...}" }
• Authenticate using your server credentials (Bearer server JWT or other configured auth).
4) Reliability & security
• Validate signatures to prevent spoofing.
• Make the webhook handler idempotent (ignore duplicate participant_joined events).
• Acknowledge quickly: return 200 OK after queuing any heavier work (dispatch call can be async).
• Log events and failures; add retry/backoff for dispatch calls.
Would you like a copy of the exact HMAC verification code and a complete Dispatch HTTP client (RestTemplate/WebClient) to drop into your Spring Boot app?refined-airport-68974
12/29/2025, 6:59 AMtall-airline-16756
12/29/2025, 6:59 AM@RestController at /webhook/livekit) are HTTP callbacks from the LiveKit server for room-level events (e.g., participant_joined). They run on your backend and are used for orchestration, logging, dispatching agents, etc.
• Agent event handlers in your Python app (@session.on(...), @room.on(...)) run inside the agent process and handle in-room, real-time events (media, transcriptions, track subscriptions).
How they work together:
• When a participant joins, LiveKit can both send a webhook to your Java endpoint and deliver in-room events to your Python agent. Those are independent paths — adding the Java webhook endpoint will not interfere with the Python agent’s handlers.
• Use webhooks for orchestration (e.g., call Agent Dispatch API when roomName matches). Use the Python agent for real-time interaction logic (transcription, responses).
• Add idempotency/coordination (DB flags or short-lived locks) if both systems might perform the same action (to avoid duplicates).
If you want, I can:
• Provide a Spring WebClient/RestTemplate snippet to call the Agent Dispatch API from your webhook handler, or
• Show a simple DB/lock pattern to coordinate actions between the webhook service and the Python agent. Which would you prefer? Sources: server-sdk-ruby | livekit/livekit_composite | DeepWiki | examples/survey | livekit/livekit_composite | DeepWiki | Agent session | LiveKit docs | server-sdk-js | livekit/livekit_composite | DeepWikirefined-airport-68974
12/29/2025, 7:04 AMtall-airline-16756
12/29/2025, 7:04 AMrefined-airport-68974
12/29/2025, 7:05 AMtall-airline-16756
12/29/2025, 7:06 AM// 1) generate participant token
String token = generateParticipantToken(...);
// 2) call Agent Dispatch API (async or fire-and-forget is recommended)
String dispatchBody = String.format("{\"room\":\"%s\",\"agent_name\":\"%s\",\"metadata\":\"%s\"}",
roomName, "support-agent", "{\"source\":\"pre-token\"}");
HttpRequest dispatchReq = HttpRequest.newBuilder()
.uri(URI.create(dispatchUrl))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + SERVER_JWT)
.POST(HttpRequest.BodyPublishers.ofString(dispatchBody))
.build();
// fire-and-forget:
HttpClient.newHttpClient().sendAsync(dispatchReq, HttpResponse.BodyHandlers.discarding())
.whenComplete((r, ex) -> {
if (ex != null) { /* log + retry/backoff as needed */ }
});
// 3) return token to client immediately
return ResponseEntity.ok(Map.of("token", token));
Would you like a Spring WebClient or RestTemplate example and a recommended idempotency key pattern for the dispatch call?refined-airport-68974
12/29/2025, 7:17 AMtall-airline-16756
12/29/2025, 7:18 AMAccessToken serverToken = new AccessToken(API_KEY, API_SECRET);
serverToken.setIdentity("server");
serverToken.setValidFor(Duration.ofHours(1));
String SERVER_JWT = serverToken.toJwt();
• Using them together (Java 11+ HttpClient example):
String dispatchUrl = "https://<your-livekit-host>/twirp/livekit.AgentDispatch/CreateDispatch";
String body = String.format("{\"room\":\"%s\",\"agent_name\":\"%s\",\"metadata\":\"%s\"}",
roomName, agentName, metadataJson);
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(dispatchUrl))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + SERVER_JWT)
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpClient.newHttpClient()
.sendAsync(req, HttpResponse.BodyHandlers.ofString())
.thenAccept(resp -> {
// check resp.statusCode()
});
Notes:
• Generate SERVER_JWT per your server process; short TTL (e.g., 1 hour) is recommended.
• The AgentDispatch endpoint accepts the request even if the room doesn't yet exist; the agent will join once the room is available.
• Make dispatch calls idempotent and non-blocking (async + retry/backoff).
Tell me your deployment (LiveKit Cloud or self-hosted) and I’ll give the exact dispatchUrl for your setup and a ready Spring WebClient snippet.
Sources: Server-side | livekit/livekit_composite | DeepWiki | Agent dispatch | LiveKit docs | Agents Framework | livekit/livekit_composite | DeepWikirefined-airport-68974
12/29/2025, 7:19 AMpackage com.oneassist.jcm.proxy.livekit;
import com.oneassist.jcm.api.dto.response.VoiceAccessTokenResponseDto;
import io.livekit.server.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;
import java.security.SecureRandom;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
/**
* Spring Boot Application and REST Controller for generating LiveKit Tokens.
* To run:
* 1. Ensure the required Maven dependencies (livekit-server-sdk, spring-boot-starter-web) are included.
* 2. Set environment variables: LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET.
* 3. Run the main method: mvn spring-boot:run
*/
@Component
@Slf4j
public class LivekitProxy {
@Value("${livekit_api_key}")
private String apiKey;
@Value("${livekit_api_secret}")
private String apiSecret;
@Value("${livekit_url}")
private String livekitUrl;
private static final Set<String> VALID_LANGUAGES = Set.of(
"hi-IN", "en-IN", "bn-IN", "gu-IN", "kn-IN", "ml-IN", "mr-IN", "or-IN", "pa-IN", "ta-IN", "te-IN"
);
private static final String DEFAULT_LANGUAGE = "hi-IN";
private static final String DEFAULT_VOICE = "anushka";
private final SecureRandom random = new SecureRandom();
public VoiceAccessTokenResponseDto generateLivekitToken(Integer chatId, String name, String agentType) {
try {
// Validate and normalize agentType
if (agentType == null || agentType.trim().isEmpty()) {
agentType = List.of("sarvam", "gemini")
.get(ThreadLocalRandom.current().nextInt(3));
}
agentType = agentType.toLowerCase().trim();
// Validate agentType - only allow 'gemini' or 'sarvam'
if (!agentType.equals("gemini") && !agentType.equals("sarvam")) {
log.warn("Invalid agentType '{}' provided, defaulting to 'sarvam'", agentType);
agentType = "sarvam";
}
final String language =DEFAULT_LANGUAGE;
final String voice = DEFAULT_VOICE;
// Include agent type prefix in room name for routing
// LiveKit will route rooms to agents based on:
// 1. Room name patterns (prefix matching: "gemini-" -> "gemini-agent", "sarvam-" -> "sarvam-agent")
// 2. Agent names set in agent code: "gemini-agent" and "sarvam-agent"
String roomBase = agentType + "-conversation-ai-" + generateRandomHex(8);
boolean hasSuffix = false;
for (String lang : VALID_LANGUAGES) {
if (roomBase.contains("-" + lang) || roomBase.endsWith("-" + lang)) {
hasSuffix = true;
break;
}
}
String roomName;
if (!hasSuffix) {
if (chatId != null && chatId!=0) {
roomName = String.format("%s-%s-%s-%s", roomBase, chatId, language, voice);
} else {
roomName = String.format("%s-%s-%s", roomBase, language, voice);
}
} else {
roomName = roomBase;
}
String participantName = name != null && !name.isEmpty()
? name
: "user-" + generateRandomHex(4);
<http://log.info|log.info>("Generating token for participant: {} in room: {} (agentType: {}, language: {}, voice: {}, chat_id: {})", participantName, roomName, agentType, language, voice, chatId);
String agentName = agentType + "-agent";
AccessToken token = new AccessToken(apiKey, apiSecret);
token.setTtl(600);
token.setIdentity(participantName);
token.setName(participantName);
token.addGrants(
new RoomJoin(true),
new Room(roomName),
new CanPublish(true),
new CanSubscribe(true),
new CanPublishData(true)
);
String jwt = token.toJwt();
<http://log.info|log.info>("Token generated for room: {} with agent_type: {} (agent_name: {})", roomName, agentType, agentName);
// 5. Prepare response (convert URL to wss/https if needed)
String safeUrl = livekitUrl.replace("ws://", "wss://").replace("http://", "https://");
VoiceAccessTokenResponseDto.VoiceAccessTokenResponseDtoBuilder responseBuilder = VoiceAccessTokenResponseDto.builder()
.authToken(jwt)
.wssUrl(safeUrl)
.roomName(roomName)
.language(language)
.voice(voice)
.agentName(agentName); // Include agent_name in response
if (chatId != null && chatId!=0) {
responseBuilder.chatId(chatId);
}
VoiceAccessTokenResponseDto responseDto = responseBuilder.build();
<http://log.info|log.info>("Token generated successfully for room: {}", roomName);
return responseDto;
} catch (Exception e) {
log.error("Exception while generating token," , e);
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to generate token: " + e.getMessage());
}
}
private String generateRandomHex(int length) {
byte[] bytes = new byte[length];
random.nextBytes(bytes);
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}tall-airline-16756
12/29/2025, 7:19 AMdispatchUrl from your livekitUrl.
• Generates a server JWT (SERVER_JWT) using your API key/secret.
• Sends an async POST to the AgentDispatch CreateDispatch endpoint with idempotency and basic retry logging.
• Keeps your existing token-generation behavior and response DTO unchanged.
Drop this into your project (imports included). Adjust logging, retry/backoff, and secret handling as needed.
package com.oneassist.jcm.proxy.livekit;
import com.oneassist.jcm.api.dto.response.VoiceAccessTokenResponseDto;
import io.livekit.server.AccessToken;
import io.livekit.server.CanPublish;
import io.livekit.server.CanPublishData;
import io.livekit.server.CanSubscribe;
import io.livekit.server.Room;
import io.livekit.server.RoomJoin;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;
import org.json.JSONObject;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
@Component
@Slf4j
public class LivekitProxy {
@Value("${livekit_api_key}")
private String apiKey;
@Value("${livekit_api_secret}")
private String apiSecret;
@Value("${livekit_url}")
private String livekitUrl;
private static final Set<String> VALID_LANGUAGES = Set.of(
"hi-IN", "en-IN", "bn-IN", "gu-IN", "kn-IN", "ml-IN", "mr-IN", "or-IN", "pa-IN", "ta-IN", "te-IN"
);
private static final String DEFAULT_LANGUAGE = "hi-IN";
private static final String DEFAULT_VOICE = "anushka";
private final SecureRandom random = new SecureRandom();
private final HttpClient httpClient = HttpClient.newHttpClient();
public VoiceAccessTokenResponseDto generateLivekitToken(Integer chatId, String name, String agentType) {
try {
// Validate and normalize agentType
if (agentType == null || agentType.trim().isEmpty()) {
agentType = List.of("sarvam", "gemini")
.get(ThreadLocalRandom.current().nextInt(2));
}
agentType = agentType.toLowerCase().trim();
if (!agentType.equals("gemini") && !agentType.equals("sarvam")) {
log.warn("Invalid agentType '{}' provided, defaulting to 'sarvam'", agentType);
agentType = "sarvam";
}
final String language = DEFAULT_LANGUAGE;
final String voice = DEFAULT_VOICE;
String roomBase = agentType + "-conversation-ai-" + generateRandomHex(8);
boolean hasSuffix = false;
for (String lang : VALID_LANGUAGES) {
if (roomBase.contains("-" + lang) || roomBase.endsWith("-" + lang)) {
hasSuffix = true;
break;
}
}
String roomName;
if (!hasSuffix) {
if (chatId != null && chatId != 0) {
roomName = String.format("%s-%s-%s-%s", roomBase, chatId, language, voice);
} else {
roomName = String.format("%s-%s-%s", roomBase, language, voice);
}
} else {
roomName = roomBase;
}
String participantName = name != null && !name.isEmpty()
? name
: "user-" + generateRandomHex(4);
<http://log.info|log.info>("Generating token for participant: {} in room: {} (agentType: {}, language: {}, voice: {}, chat_id: {})",
participantName, roomName, agentType, language, voice, chatId);
String agentName = agentType + "-agent";
AccessToken token = new AccessToken(apiKey, apiSecret);
token.setTtl(600);
token.setIdentity(participantName);
token.setName(participantName);
token.addGrants(
new RoomJoin(true),
new Room(roomName),
new CanPublish(true),
new CanSubscribe(true),
new CanPublishData(true)
);
String jwt = token.toJwt();
<http://log.info|log.info>("Token generated for room: {} with agent_type: {} (agent_name: {})", roomName, agentType, agentName);
// Prepare response
String safeUrl = livekitUrl.replace("ws://", "wss://").replace("http://", "https://");
VoiceAccessTokenResponseDto.VoiceAccessTokenResponseDtoBuilder responseBuilder = VoiceAccessTokenResponseDto.builder()
.authToken(jwt)
.wssUrl(safeUrl)
.roomName(roomName)
.language(language)
.voice(voice)
.agentName(agentName);
if (chatId != null && chatId!=0) {
responseBuilder.chatId(chatId);
}
VoiceAccessTokenResponseDto responseDto = responseBuilder.build();
// --- NEW: dispatch agent BEFORE returning token (async fire-and-forget) ---
try {
String dispatchUrl = buildDispatchUrl(livekitUrl);
String serverJwt = buildServerJwt();
String metadata = new JSONObject().put("source", "pre-token").put("chatId", chatId == null ? JSONObject.NULL : chatId).toString();
JSONObject body = new JSONObject();
body.put("room", roomName);
body.put("agent_name", agentName);
body.put("metadata", metadata);
// Optional: include an idempotency key to avoid duplicate dispatches
String idempotencyKey = "dispatch-" + roomName;
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(dispatchUrl))
.timeout(Duration.ofSeconds(5))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + serverJwt)
.header("Idempotency-Key", idempotencyKey)
.POST(HttpRequest.BodyPublishers.ofString(body.toString()))
.build();
// fire-and-forget; log result or schedule retry on failure
httpClient.sendAsync(req, HttpResponse.BodyHandlers.ofString())
.thenAccept(resp -> {
int status = resp.statusCode();
if (status >= 200 && status < 300) {
log.info("Agent dispatch request succeeded for room {} (agent={})", roomName, agentName);
} else {
log.warn("Agent dispatch failed for room {} (status={}, body={})", roomName, status, resp.body());
// optional: enqueue retry
}
})
.exceptionally(ex -> {
log.error("Agent dispatch request error for room {}", roomName, ex);
return null;
});
} catch (Exception ex) {
log.error("Failed to call agent dispatch (continuing to return token) for room {}", roomName, ex);
// do NOT fail token issuance if dispatch fails here
}
<http://log.info|log.info>("Token generated successfully for room: {}", roomName);
return responseDto;
} catch (Exception e) {
log.error("Exception while generating token,", e);
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to generate token: " + e.getMessage());
}
}
private String buildDispatchUrl(String livekitUrl) {
// Normalize livekitUrl and construct twirp endpoint
String normalized = livekitUrl.replace("ws://", "https://").replace("wss://", "https://").replace("http://", "https://");
// remove trailing slash if any
if (normalized.endsWith("/")) {
normalized = normalized.substring(0, normalized.length() - 1);
}
return normalized + "/twirp/livekit.AgentDispatch/CreateDispatch";
}
private String buildServerJwt() {
// Server JWT for authenticating control API calls
AccessToken serverToken = new AccessToken(apiKey, apiSecret);
serverToken.setIdentity("server");
serverToken.setTtl(3600); // 1 hour
return serverToken.toJwt();
}
private String generateRandomHex(int length) {
byte[] bytes = new byte[length];
random.nextBytes(bytes);
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}
Notes:
• This keeps token issuance resilient: a dispatch failure logs but does not prevent returning the token.
• The dispatch request is asynchronous; if you need to ensure the agent is in-room before the client connects, change to synchronous send and wait for success (with timeout) — but that increases latency.
• Add stronger idempotency and retry/backoff for production.
• Ensure your LiveKit control endpoint uses HTTPS and your apiKey/apiSecret are secured (env vars).
• If your LiveKit Cloud URL uses a different domain pattern, adjust buildDispatchUrl accordingly.refined-airport-68974
12/29/2025, 7:48 AMtall-airline-16756
12/29/2025, 7:48 AMrefined-airport-68974
12/29/2025, 7:56 AMtall-airline-16756
12/29/2025, 7:56 AMwebhook / webhook_auth in your LiveKit config to point to the same URL and secret.
• Use a shared webhook secret (HMAC) or the LiveKit-specified header so your server can verify events.
2) Where this runs
• Add the controller to your Spring Boot service that already issues tokens. It does not affect your Python agent; it simply triggers dispatch when a join happens.
3) Webhook handling behavior
• On receiving participant_joined: check room name pattern (e.g., startsWith("sarvam-")). If match, call AgentDispatch CreateDispatch with { room, agent_name, metadata }.
• Make dispatch idempotent (use an Idempotency-Key header or persist a dispatch flag per room) so retry/dedup is safe.
• Prefer async dispatch call (fire-and-forget) and log results. If you need to guarantee the agent is present, call synchronously with timeout and retries (but this increases request latency).
4) Spring Boot controller (drop into your app)
• Replace placeholders: LIVEKIT_CONTROL_URL (your livekit host), LIVEKIT_WEBHOOK_SECRET, API_KEY, API_SECRET, AGENT_NAME, and pattern check.
@RestController
@RequestMapping("/webhook/livekit")
public class LivekitWebhookController {
@Value("${livekit_control_url}") // e.g. https://<your-project>.livekit.cloud
private String livekitControlUrl;
@Value("${livekit_api_key}")
private String apiKey;
@Value("${livekit_api_secret}")
private String apiSecret;
@Value("${livekit_webhook_secret}")
private String webhookSecret;
@Value("${dispatch_agent_name}")
private String agentName; // e.g. "sarvam-agent"
private final HttpClient httpClient = HttpClient.newHttpClient();
@PostMapping
public ResponseEntity<String> handleWebhook(@RequestHeader Map<String,String> headers,
@RequestBody String body) {
try {
// 1) verify signature (example header "X-LiveKit-Signature" using HMAC-SHA256)
String sigHeader = headers.getOrDefault("x-livekit-signature", headers.getOrDefault("X-LiveKit-Signature",""));
if (!verifyHmacSha256(body, sigHeader, webhookSecret)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("invalid signature");
}
// 2) parse payload
JSONObject json = new JSONObject(body);
String event = json.optString("event", json.optString("type", ""));
if (!"participant_joined".equals(event) && !"participant.joined".equals(event)) {
return ResponseEntity.ok("ignored");
}
JSONObject participant = json.optJSONObject("participant");
if (participant == null) return ResponseEntity.ok("no participant");
String room = participant.optString("room");
if (room == null || room.isEmpty()) return ResponseEntity.ok("no room");
// 3) decide whether to dispatch (room naming convention)
if (!room.startsWith("sarvam-") && !room.startsWith("support-")) {
return ResponseEntity.ok("no dispatch needed");
}
// 4) build dispatch request
String dispatchUrl = buildDispatchUrl(livekitControlUrl);
String serverJwt = buildServerJwt(apiKey, apiSecret);
JSONObject bodyJson = new JSONObject();
bodyJson.put("room", room);
bodyJson.put("agent_name", agentName);
bodyJson.put("metadata", new JSONObject().put("trigger", "webhook").put("participantSid", participant.optString("sid")).toString());
String idempotencyKey = "dispatch-" + room; // simple idempotency; consider DB flag
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(dispatchUrl))
.timeout(Duration.ofSeconds(5))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + serverJwt)
.header("Idempotency-Key", idempotencyKey)
.POST(HttpRequest.BodyPublishers.ofString(bodyJson.toString()))
.build();
// 5) async send; log result
httpClient.sendAsync(req, HttpResponse.BodyHandlers.ofString())
.thenAccept(resp -> {
int status = resp.statusCode();
if (status >= 200 && status < 300) {
log.info("Agent dispatch succeeded for room {} (agent={})", room, agentName);
} else {
log.warn("Agent dispatch failed for room {} status={} body={}", room, status, resp.body());
}
})
.exceptionally(ex -> {
log.error("Agent dispatch error for room {}", room, ex);
return null;
});
return ResponseEntity.ok("dispatch triggered");
} catch (Exception e) {
log.error("webhook handling error", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("error");
}
}
private boolean verifyHmacSha256(String payload, String signatureHeader, String secret) {
if (signatureHeader == null || signatureHeader.isEmpty()) return false;
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] expected = HexFormat.of().parseHex(bytesToHex(mac.doFinal(payload.getBytes(StandardCharsets.UTF_8))));
// signatureHeader may be hex or base64 depending on your LiveKit config; adapt parsing accordingly
String expectedHex = bytesToHex(mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)));
return signatureHeader.equalsIgnoreCase(expectedHex);
} catch (Exception ex) {
log.error("signature verify error", ex);
return false;
}
}
private String buildDispatchUrl(String livekitUrl) {
String normalized = livekitUrl.replace("ws://", "https://").replace("wss://", "https://").replace("http://", "https://");
if (normalized.endsWith("/")) normalized = normalized.substring(0, normalized.length()-1);
return normalized + "/twirp/livekit.AgentDispatch/CreateDispatch";
}
private String buildServerJwt(String apiKey, String apiSecret) {
AccessToken serverToken = new AccessToken(apiKey, apiSecret);
serverToken.setIdentity("server");
serverToken.setTtl(3600); // 1 hour
return serverToken.toJwt();
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b: bytes) sb.append(String.format("%02x", b));
return sb.toString();
}
}
5) Best practices & notes
• Signature format: confirm whether LiveKit signs webhooks as hex or base64 and adapt verifyHmacSha256 accordingly.
• Idempotency: use a small DB table or cache to mark dispatched rooms to avoid duplicate dispatches.
• Quick response: return 200 quickly; do async dispatch to avoid webhook timeouts.
• Security: restrict the webhook endpoint (IP allowlist, TLS) and keep the webhook secret and API credentials safe.
• Monitoring: log received events and dispatch outcomes; attach alerting for repeated failures.
If you want, I can:
• adapt the verifyHmacSha256 method to match the exact header encoding your LiveKit instance uses (paste one example webhook header), or
• provide a full RestTemplate / WebClient implementation instead of HttpClient, or
• add example DB idempotency code using Redis or your existing DB.
Which would you like next?refined-airport-68974
12/29/2025, 9:52 AMtall-airline-16756
12/29/2025, 9:52 AMlivekit_webhook_secret in your app.
• Self‑hosted LiveKit
◦ In your LiveKit server config (typically the YAML config), set webhook.auth / webhook_secret (or webhook_auth) to the secret you choose. Restart LiveKit. Use that same secret in your app.
How to use it:
• Store the secret in your environment or Spring config (e.g., application.properties):
livekit_webhook_secret=your_secret_here
• Verify incoming webhooks by computing HMAC-SHA256(payload, secret) and comparing with the signature header LiveKit sends (check your deployment whether the header is hex or base64 and match that format).
Security tips:
• Keep the secret out of code (env vars, secrets manager).
• Return 401 on signature mismatch.
• Optionally restrict webhook endpoint by IP, TLS, or mutual TLS.
• Use idempotency to handle retries.
If you tell me whether you’re on LiveKit Cloud or self-hosted, I’ll give the exact place in the console or the config snippet and an HMAC verification snippet matching LiveKit’s header encoding.