Repository: livekit/agents
File: livekit-plugins/livekit-plugins-google/livekit/plugins/google/realtime/realtime_api.py (Python)

Code before:
    def generate_reply(
        self, *, instructions: NotGivenOr[str] = NOT_GIVEN
    ) -> asyncio.Future[llm.GenerationCreatedEvent]:
        if self._pending_generation_fut and not self._pending_generation_fut.done():
            logger.warning(
                "generate_reply called while another generation is pending, cancelling previous."
            )
            self._pending_generation_fut.cancel("Superseded by new generate_reply call")

        fut = asyncio.Future[llm.GenerationCreatedEvent]()
        self._pending_generation_fut = fut

        if self._in_user_activity:
            self._send_client_event(
                types.LiveClientRealtimeInput(
                    activity_end=types.ActivityEnd(),
                )
            )
            self._in_user_activity = False

        # Gemini requires the last message to end with user's turn
        # so we need to add a placeholder user turn in order to trigger a new generation
        if is_given(instructions):
            turns = []
            turns.append(types.Content(parts=[types.Part(text=instructions)], role="model"))
        turns.append(types.Content(parts=[types.Part(text=".")], role="user"))
        self._send_client_event(types.LiveClientContent(turns=turns, turn_complete=True))

        def _on_timeout() -> None:
            if not fut.done():
                fut.set_exception(
                    llm.RealtimeError(
                        "generate_reply timed out waiting for generation_created event."
                    )
                )
                if self._pending_generation_fut is fut:
                    self._pending_generation_fut = None

        timeout_handle = asyncio.get_event_loop().call_later(5.0, _on_timeout)
        fut.add_done_callback(lambda _: timeout_handle.cancel())

        return fut

    def start_user_activity(self) -> None:
        if not self._manual_activity_detection:
            return

        if not self._in_user_activity:
            self._in_user_activity = True
            self._send_client_event(
                types.LiveClientRealtimeInput(

Review comment (refactor):
I think this might result in duplicate turns again? so when a text message is received, it will perform update_chat_context first, then call generate_reply with `instructions=None`.. in that case, we'd add the chat context but also the extra `.`?

Code after:
    def generate_reply(
        self, *, instructions: NotGivenOr[str] = NOT_GIVEN
    ) -> asyncio.Future[llm.GenerationCreatedEvent]:
        if self._pending_generation_fut and not self._pending_generation_fut.done():
            logger.warning(
                "generate_reply called while another generation is pending, cancelling previous."
            )
            self._pending_generation_fut.cancel("Superseded by new generate_reply call")

        fut = asyncio.Future[llm.GenerationCreatedEvent]()
        self._pending_generation_fut = fut

        if self._in_user_activity:
            self._send_client_event(
                types.LiveClientRealtimeInput(
                    activity_end=types.ActivityEnd(),
                )
            )
            self._in_user_activity = False

        # Gemini requires the last message to end with user's turn
        # so we need to add a placeholder user turn in order to trigger a new generation
        turns = []
        if is_given(instructions):
            turns.append(types.Content(parts=[types.Part(text=instructions)], role="model"))
        turns.append(types.Content(parts=[types.Part(text=".")], role="user"))
        self._send_client_event(types.LiveClientContent(turns=turns, turn_complete=True))

        def _on_timeout() -> None:
            if not fut.done():
                fut.set_exception(
                    llm.RealtimeError(
                        "generate_reply timed out waiting for generation_created event."
                    )
                )
                if self._pending_generation_fut is fut:
                    self._pending_generation_fut = None

        timeout_handle = asyncio.get_event_loop().call_later(5.0, _on_timeout)
        fut.add_done_callback(lambda _: timeout_handle.cancel())

        return fut

    def start_user_activity(self) -> None:
        if not self._manual_activity_detection:
            return

        if not self._in_user_activity:
            self._in_user_activity = True
            self._send_client_event(
                types.LiveClientRealtimeInput(