diff --git a/cmd/lk/simulate.go b/cmd/lk/simulate.go index c0fd6e49..be1bce5a 100644 --- a/cmd/lk/simulate.go +++ b/cmd/lk/simulate.go @@ -175,6 +175,18 @@ var simulateAudioCommand = &cli.Command{ Name: "packet-loss", Usage: "Drop packets from the simulated user's audio track", }, + &cli.StringFlag{ + Name: "to-phone-number", + Usage: "Call this `NUMBER` over SIP and simulate against whoever answers, instead of dispatching an agent. Requires --from-phone-number and --scenarios.", + }, + &cli.StringFlag{ + Name: "from-trunk", + Usage: "Outbound SIP trunk `ID` to place --to-phone-number calls through. Without it, calls go out from a LiveKit phone number.", + }, + &cli.StringFlag{ + Name: "from-phone-number", + Usage: "Caller `NUMBER` for --to-phone-number calls: a LiveKit phone number the project owns, or a number on --from-trunk", + }, }, } @@ -410,7 +422,7 @@ type simulateConfig struct { scenarioGroup *livekit.ScenarioGroup scenariosPath string // path to the --scenarios file (empty when generating from source) viewModeRunID string // non-empty when `view` opens a pre-existing run - liveAgent bool // --agent-name: run against an already-running agent, don't spawn one + liveAgent bool // --agent-name or --to-phone-number: run against an already-running agent, don't spawn one warnings []string // config-level warnings surfaced at setup (e.g. ignored flags) // impairments on the simulated user's audio, set only in SIMULATION_MODE_AUDIO @@ -418,6 +430,11 @@ type simulateConfig struct { lowQualityMicrophone bool packetLoss bool + // --to-phone-number: dial this number over SIP in place of dispatching agentName + sipCallTo string + sipTrunkID string + sipNumber string + // TODO (steveyoon): add agent deployment support // agentDeployment string } @@ -522,6 +539,22 @@ func runSimulate(ctx context.Context, cmd *cli.Command, simulationMode livekit.S samples := int32(cmd.Int("samples")) passRate := cmd.Float("pass-rate") liveAgentName := cmd.String("agent-name") + var phoneNumber, fromTrunk, fromPhoneNumber string + if simulationMode == livekit.SimulationMode_SIMULATION_MODE_AUDIO { + phoneNumber = cmd.String("to-phone-number") + fromTrunk = cmd.String("from-trunk") + fromPhoneNumber = cmd.String("from-phone-number") + } + if fromTrunk != "" && phoneNumber == "" { + return fmt.Errorf("--from-trunk requires --to-phone-number") + } + if fromPhoneNumber != "" && phoneNumber == "" { + return fmt.Errorf("--from-phone-number requires --to-phone-number") + } + // every call needs a caller: the number simulations are placed from + if phoneNumber != "" && fromPhoneNumber == "" { + return fmt.Errorf("--to-phone-number requires --from-phone-number") + } // never auto-discovered: an explicit --scenarios file is the source of // truth, otherwise scenarios are generated from the agent's source @@ -547,7 +580,13 @@ func runSimulate(ctx context.Context, cmd *cli.Command, simulationMode livekit.S // --agent-name (even empty) means: run against an already-running agent, // don't spawn one. https://docs.livekit.io/agents/server/agent-dispatch/#automatic - if cmd.IsSet("agent-name") { + if phoneNumber != "" { + // nothing is spawned, so there's no source to generate scenarios from. + if scenariosPath == "" { + return fmt.Errorf("--to-phone-number requires --scenarios (no source to generate scenarios from when calling a number)") + } + liveAgent = true + } else if cmd.IsSet("agent-name") { // nothing is spawned, so there's no source to generate scenarios from. if scenariosPath == "" { return fmt.Errorf("--agent-name requires --scenarios (no source to generate scenarios from when running against a live agent)") @@ -628,6 +667,9 @@ func runSimulate(ctx context.Context, cmd *cli.Command, simulationMode livekit.S simCfg.backgroundNoise = cmd.Bool("background-noise") simCfg.lowQualityMicrophone = cmd.Bool("low-quality-microphone") simCfg.packetLoss = cmd.Bool("packet-loss") + simCfg.sipCallTo = phoneNumber + simCfg.sipTrunkID = fromTrunk + simCfg.sipNumber = fromPhoneNumber } if !isInteractive() { @@ -801,6 +843,9 @@ func createSimulationRun(ctx context.Context, c *simulateConfig) (string, *livek BackgroundNoise: c.backgroundNoise, LowQualityMicrophone: c.lowQualityMicrophone, PacketLoss: c.packetLoss, + SipCallTo: c.sipCallTo, + SipTrunkId: c.sipTrunkID, + SipNumber: c.sipNumber, Ci: ciFromEnv(), } if c.concurrency > 0 { diff --git a/go.mod b/go.mod index 873cb981..90eee86b 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 github.com/joho/godotenv v1.5.1 github.com/klauspost/compress v1.20.1 - github.com/livekit/protocol v1.53.1-0.20261022182030-f1ed0814615a + github.com/livekit/protocol v1.53.2-0.20261026205625-e87b8f61d18c github.com/livekit/server-sdk-go/v2 v2.19.0 github.com/mattn/go-isatty v0.0.22 github.com/moby/moby/client v0.4.1 @@ -132,7 +132,7 @@ require ( github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/gammazero/deque v1.2.1 // indirect github.com/getkin/kin-openapi v0.144.0 // indirect - github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/go-jose/go-jose/v4 v4.1.5 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.23.1 // indirect github.com/go-openapi/swag/jsonname v0.26.1 // indirect diff --git a/go.sum b/go.sum index 730122e1..f9559682 100644 --- a/go.sum +++ b/go.sum @@ -253,8 +253,8 @@ github.com/gammazero/deque v1.2.1 h1:9fnQVFCCZ9/NOc7ccTNqzoKd1tCWOqeI05/lPqFPMGQ github.com/gammazero/deque v1.2.1/go.mod h1:5nSFkzVm+afG9+gy0VIowlqVAW4N8zNcMne+CMQVD2g= github.com/getkin/kin-openapi v0.144.0 h1:hIRcTH+KjLfkLpYU6bSSfdFpi0fZi1fp+hSPi4aQu9Y= github.com/getkin/kin-openapi v0.144.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY= -github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= -github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-jose/go-jose/v4 v4.1.5 h1:RjgjO2LOtWOJKUC5wpwY9LR3B3vwVAz6JS2YHfYU6eA= +github.com/go-jose/go-jose/v4 v4.1.5/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -364,8 +364,8 @@ github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731 h1:9x+U2HGLrSw5AT github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ= github.com/livekit/mediatransportutil v0.0.0-20260821083140-f234b534b095 h1:BcliKAXoMhl/nWmzQweQ5kmh4Qqagxl4s3Z5pvM/7AY= github.com/livekit/mediatransportutil v0.0.0-20260821083140-f234b534b095/go.mod h1:o8CFmAdrVwzJNOCsQCLUzXRjokkufNshnQHOe4fRaqU= -github.com/livekit/protocol v1.53.1-0.20261022182030-f1ed0814615a h1:+2yEcpXySowdY80dJJ5uXhiykNh6M+HE5KdScn/Kx1w= -github.com/livekit/protocol v1.53.1-0.20261022182030-f1ed0814615a/go.mod h1:kUTAI2TrTngGOiWP6MTAOkJpru6YYGd0MP9wzlDATy4= +github.com/livekit/protocol v1.53.2-0.20261026205625-e87b8f61d18c h1:vGLJnV6d5WUPsZ84DO1uHcBtiMcxHQw7R1/13kdrdo8= +github.com/livekit/protocol v1.53.2-0.20261026205625-e87b8f61d18c/go.mod h1:h9P4KCc3K715NCgCrkkFsb8/RRY1kKqOS1EIHUNRlCk= github.com/livekit/psrpc v0.8.2 h1:7tbkhBJoYlK5Csfkdv1XYbZjyAQeaEZgupvIXNCv7Dc= github.com/livekit/psrpc v0.8.2/go.mod h1:6KnU8vULoizX0Er9NNfeKJDV8ug87j3fuo19M0bhj6Q= github.com/livekit/server-sdk-go/v2 v2.19.0 h1:fZ0zmeC/5V+rXiI/MeC05EjwTfXmpaGkr/DXLG3EzgA=