From 03651c4ae59aecb528e1d02ec1e72a1db831d7b0 Mon Sep 17 00:00:00 2001 From: AssemblyAI Date: Wed, 10 Sep 2025 10:51:09 -0600 Subject: [PATCH] Project import generated by Copybara. GitOrigin-RevId: 6515d56b4717d0b33d65fb084ca7ab296fe77e26 --- assemblyai/__version__.py | 2 +- assemblyai/streaming/v3/models.py | 2 + assemblyai/types.py | 19 +++ tests/unit/factories.py | 80 ++++++++++ tests/unit/test_lemur.py | 248 ++++++++++++++++++++++++++++-- 5 files changed, 335 insertions(+), 16 deletions(-) diff --git a/assemblyai/__version__.py b/assemblyai/__version__.py index fb8a056..04207c2 100644 --- a/assemblyai/__version__.py +++ b/assemblyai/__version__.py @@ -1 +1 @@ -__version__ = "0.43.1" +__version__ = "0.44.2" diff --git a/assemblyai/streaming/v3/models.py b/assemblyai/streaming/v3/models.py index bb2507d..c585e2d 100644 --- a/assemblyai/streaming/v3/models.py +++ b/assemblyai/streaming/v3/models.py @@ -60,6 +60,8 @@ class StreamingSessionParameters(BaseModel): min_end_of_turn_silence_when_confident: Optional[int] = None max_turn_silence: Optional[int] = None format_turns: Optional[bool] = None + keyterms_prompt: Optional[List[str]] = None + filter_profanity: Optional[bool] = None class Encoding(str, Enum): diff --git a/assemblyai/types.py b/assemblyai/types.py index 5e74540..b832c6d 100644 --- a/assemblyai/types.py +++ b/assemblyai/types.py @@ -525,6 +525,7 @@ def validate_max_speakers(cls, v, info): "max_speakers_expected must be greater than or equal to min_speakers_expected" ) return v + else: @validator("max_speakers_expected") @@ -1607,6 +1608,7 @@ class Word(BaseModel): @field_validator("start", mode="before") def set_start_default(cls, v): return 0 if v is None else v + else: @validator("start", pre=True) @@ -2317,6 +2319,20 @@ class LemurUsage(BaseModel): "The number of output tokens generated by the model" +class LemurRequestDetails(BaseModel): + request_endpoint: str + temperature: float + final_model: str + max_output_size: int + created_at: datetime + transcript_ids: Optional[List[str]] = None + input_text: Optional[str] = None + questions: Optional[List[LemurQuestion]] = None + prompt: Optional[str] = None + context: Optional[Union[dict, str]] = None + answer_format: Optional[str] = None + + class BaseLemurResponse(BaseModel): request_id: str "The unique identifier of your LeMUR request" @@ -2324,6 +2340,9 @@ class BaseLemurResponse(BaseModel): usage: LemurUsage "The usage numbers for the LeMUR request" + request: Optional[LemurRequestDetails] = None + "The request details the user passed into the POST request. Optional since this only exists on the GET request." + class LemurStringResponse(BaseLemurResponse): """ diff --git a/tests/unit/factories.py b/tests/unit/factories.py index 6e66c52..aa1e381 100644 --- a/tests/unit/factories.py +++ b/tests/unit/factories.py @@ -246,6 +246,49 @@ class Meta: ) +class LemurRequestDetails(factory.Factory): + class Meta: + model = types.LemurRequestDetails + + request_endpoint = factory.Faker("text") + temperature = factory.Faker("pyfloat") + final_model = factory.Faker("text") + max_output_size = factory.Faker("pyint") + created_at = factory.Faker("iso8601") + + +class LemurTaskRequestDetails(LemurRequestDetails): + """Request details specific to LeMUR task operations""" + + request_endpoint = "/lemur/v3/task" + prompt = factory.Faker("text") + + +class LemurSummaryRequestDetails(LemurRequestDetails): + """Request details specific to LeMUR summary operations""" + + request_endpoint = "/lemur/v3/summary" + context = factory.LazyFunction(lambda: {"key": "value"}) + answer_format = factory.Faker("sentence") + + +class LemurQuestionRequestDetails(LemurRequestDetails): + """Request details specific to LeMUR question-answer operations""" + + request_endpoint = "/lemur/v3/question-answer" + questions = [ + { + "question": "What is the main topic?", + "answer_format": "short sentence", + "context": "Meeting context", + }, + { + "question": "What is the sentiment?", + "answer_options": ["positive", "negative", "neutral"], + }, + ] + + class LemurUsage(factory.Factory): class Meta: model = types.LemurUsage @@ -310,6 +353,43 @@ class Meta: request_id = factory.Faker("uuid4") usage = factory.SubFactory(LemurUsage) response = factory.Faker("text") + request = factory.SubFactory(LemurRequestDetails) + + +# Factories specifically for get_response endpoint tests (include request field) +class LemurTaskResponseWithRequest(factory.Factory): + class Meta: + model = types.LemurTaskResponse + + request_id = factory.Faker("uuid4") + usage = factory.SubFactory(LemurUsage) + response = factory.Faker("text") + request = factory.SubFactory(LemurTaskRequestDetails) + + +class LemurSummaryResponseWithRequest(factory.Factory): + class Meta: + model = types.LemurSummaryResponse + + request_id = factory.Faker("uuid4") + usage = factory.SubFactory(LemurUsage) + response = factory.Faker("text") + request = factory.SubFactory(LemurSummaryRequestDetails) + + +class LemurQuestionResponseWithRequest(factory.Factory): + class Meta: + model = types.LemurQuestionResponse + + request_id = factory.Faker("uuid4") + usage = factory.SubFactory(LemurUsage) + response = factory.List( + [ + factory.SubFactory(LemurQuestionAnswer), + factory.SubFactory(LemurQuestionAnswer), + ] + ) + request = factory.SubFactory(LemurQuestionRequestDetails) class LemurPurgeResponse(factory.Factory): diff --git a/tests/unit/test_lemur.py b/tests/unit/test_lemur.py index dd6acee..4696a98 100644 --- a/tests/unit/test_lemur.py +++ b/tests/unit/test_lemur.py @@ -907,21 +907,146 @@ def test_lemur_usage_data(httpx_mock: HTTPXMock): assert len(httpx_mock.get_requests()) == 1 -@pytest.mark.parametrize("response_type", ("string_response", "qa_response")) -def test_lemur_get_response_data(response_type, httpx_mock: HTTPXMock): +def test_lemur_get_response_data_string_response(httpx_mock: HTTPXMock): """ - Tests whether a LeMUR response data is correctly returned. + Tests whether a LeMUR string response data is correctly returned. """ request_id = "1234" - # create a mock response - if response_type == "string_response": - mock_lemur_response = factories.generate_dict_factory( - factories.LemurStringResponse + mock_lemur_response = factories.generate_dict_factory( + factories.LemurStringResponse + )() + mock_lemur_response["request_id"] = request_id + + # mock the specific endpoint + httpx_mock.add_response( + url=f"{aai.settings.base_url}{ENDPOINT_LEMUR_BASE}/{request_id}", + status_code=httpx.codes.OK, + method="GET", + json=mock_lemur_response, + ) + + # mimic the usage of the SDK + lemur = aai.Lemur() + result = lemur.get_response_data(request_id) + + # check the response + assert isinstance(result, aai.LemurStringResponse) + assert result.request_id == request_id + + # test the request field is populated correctly + assert result.request is not None + assert hasattr(result.request, "request_endpoint") + assert hasattr(result.request, "temperature") + assert hasattr(result.request, "final_model") + assert hasattr(result.request, "max_output_size") + assert hasattr(result.request, "created_at") + + # test usage field + assert result.usage is not None + assert hasattr(result.usage, "input_tokens") + assert hasattr(result.usage, "output_tokens") + + # check whether we mocked everything + assert len(httpx_mock.get_requests()) == 1 + + +def test_lemur_get_response_data_question_answer(httpx_mock: HTTPXMock): + """ + Tests whether a LeMUR question-answer response data is correctly returned with questions field. + """ + request_id = "qa-1234" + + mock_lemur_response = factories.generate_dict_factory( + factories.LemurQuestionResponse + )() + mock_lemur_response["request"] = factories.generate_dict_factory( + factories.LemurQuestionRequestDetails + )() + mock_lemur_response["request_id"] = request_id + + # mock the specific endpoint + httpx_mock.add_response( + url=f"{aai.settings.base_url}{ENDPOINT_LEMUR_BASE}/{request_id}", + status_code=httpx.codes.OK, + method="GET", + json=mock_lemur_response, + ) + + # mimic the usage of the SDK + lemur = aai.Lemur() + result = lemur.get_response_data(request_id) + + # check the response + assert isinstance(result, aai.LemurQuestionResponse) + assert result.request_id == request_id + + # test the request field is populated correctly + assert result.request is not None + assert result.request.request_endpoint == "/lemur/v3/question-answer" + assert hasattr(result.request, "temperature") + assert hasattr(result.request, "final_model") + assert hasattr(result.request, "max_output_size") + assert hasattr(result.request, "created_at") + + # test question-answer specific request fields + assert hasattr(result.request, "questions") + assert isinstance(result.request.questions, list) + assert len(result.request.questions) == 2 + + # test that questions have the right structure - one with answer_format, one with answer_options + question1, question2 = result.request.questions + assert hasattr(question1, "answer_format") + assert hasattr(question1, "context") + assert question1.question == "What is the main topic?" + assert question1.answer_format == "short sentence" + assert question1.context == "Meeting context" + + assert hasattr(question2, "answer_options") + assert question2.question == "What is the sentiment?" + assert question2.answer_options == ["positive", "negative", "neutral"] + + # test that qa-specific fields are None for other operation types + assert result.request.prompt is None # task-specific field + assert result.request.context is None # summary/action_items-specific field + assert result.request.answer_format is None # summary/action_items-specific field + + # test usage field + assert result.usage is not None + assert hasattr(result.usage, "input_tokens") + assert hasattr(result.usage, "output_tokens") + + # test response structure for question-answer + assert isinstance(result.response, list) + assert len(result.response) == 2 + for answer in result.response: + assert hasattr(answer, "question") + assert hasattr(answer, "answer") + + # check whether we mocked everything + assert len(httpx_mock.get_requests()) == 1 + + +@pytest.mark.parametrize("response_type", ("summary", "task")) +def test_lemur_get_response_data_additional_types(response_type, httpx_mock: HTTPXMock): + """ + Tests whether additional LeMUR response types are correctly returned with request details. + """ + request_id = "5678" + + # create a mock response - get_response_data returns LemurStringResponse but with different request details + mock_lemur_response = factories.generate_dict_factory( + factories.LemurStringResponse + )() + + # Override the request details based on response type + if response_type == "summary": + mock_lemur_response["request"] = factories.generate_dict_factory( + factories.LemurSummaryRequestDetails )() - else: - mock_lemur_response = factories.generate_dict_factory( - factories.LemurQuestionResponse + else: # task + mock_lemur_response["request"] = factories.generate_dict_factory( + factories.LemurTaskRequestDetails )() mock_lemur_response["request_id"] = request_id @@ -938,13 +1063,106 @@ def test_lemur_get_response_data(response_type, httpx_mock: HTTPXMock): lemur = aai.Lemur() result = lemur.get_response_data(request_id) - # check the response - if response_type == "string_response": - assert isinstance(result, aai.LemurStringResponse) - else: - assert isinstance(result, aai.LemurQuestionResponse) + # check the response type - get_response_data returns LemurStringResponse for all string responses + assert isinstance(result, aai.LemurStringResponse) assert result.request_id == request_id + # test the request field is populated correctly for all response types + assert result.request is not None + assert hasattr(result.request, "request_endpoint") + assert hasattr(result.request, "temperature") + assert hasattr(result.request, "final_model") + assert hasattr(result.request, "max_output_size") + assert hasattr(result.request, "created_at") + + # test type-specific request fields + if response_type == "summary": + assert result.request.request_endpoint == "/lemur/v3/summary" + assert result.request.context is not None + assert result.request.answer_format is not None + assert result.request.prompt is None # task-specific field + assert result.request.questions is None # qa-specific field + else: # task + assert result.request.request_endpoint == "/lemur/v3/task" + assert result.request.prompt is not None + assert result.request.context is None # summary-specific field + assert result.request.answer_format is None # summary-specific field + assert result.request.questions is None # qa-specific field + + # test usage field + assert result.usage is not None + assert hasattr(result.usage, "input_tokens") + assert hasattr(result.usage, "output_tokens") + + # test string response field + assert isinstance(result.response, str) + # check whether we mocked everything assert len(httpx_mock.get_requests()) == 1 + + +def test_lemur_get_response_data_with_optional_request_fields(httpx_mock: HTTPXMock): + """ + Tests that optional fields in LemurRequestDetails are handled correctly. + """ + request_id = "test-optional-fields" + + mock_lemur_response = factories.generate_dict_factory( + factories.LemurStringResponse + )() + mock_lemur_response["request_id"] = request_id + + # Add optional fields to request details + mock_lemur_response["request"]["transcript_ids"] = ["transcript_1", "transcript_2"] + mock_lemur_response["request"]["input_text"] = "Test input text" + mock_lemur_response["request"]["questions"] = [ + {"question": "What is this about?", "answer_format": "short"} + ] + mock_lemur_response["request"]["prompt"] = "Test prompt" + mock_lemur_response["request"]["context"] = {"key": "value"} + mock_lemur_response["request"]["answer_format"] = "bullet points" + + httpx_mock.add_response( + url=f"{aai.settings.base_url}{ENDPOINT_LEMUR_BASE}/{request_id}", + status_code=httpx.codes.OK, + method="GET", + json=mock_lemur_response, + ) + + lemur = aai.Lemur() + result = lemur.get_response_data(request_id) + + assert isinstance(result, aai.LemurStringResponse) + assert result.request_id == request_id + + # test that optional fields are present + assert result.request.transcript_ids == ["transcript_1", "transcript_2"] + assert result.request.input_text == "Test input text" + assert result.request.questions is not None + assert result.request.prompt == "Test prompt" + assert result.request.context == {"key": "value"} + assert result.request.answer_format == "bullet points" + + assert len(httpx_mock.get_requests()) == 1 + + +def test_lemur_get_response_data_fails(httpx_mock: HTTPXMock): + """ + Tests that get_response_data properly handles API errors. + """ + request_id = "error-request-id" + + httpx_mock.add_response( + url=f"{aai.settings.base_url}{ENDPOINT_LEMUR_BASE}/{request_id}", + status_code=httpx.codes.NOT_FOUND, + method="GET", + json={"error": "Request not found"}, + ) + + lemur = aai.Lemur() + + with pytest.raises(aai.LemurError): + lemur.get_response_data(request_id) + + assert len(httpx_mock.get_requests()) == 1