diff --git a/assemblyai/__init__.py b/assemblyai/__init__.py index a765353..bc879b0 100644 --- a/assemblyai/__init__.py +++ b/assemblyai/__init__.py @@ -52,6 +52,9 @@ TTSWordBoundariesEvent, ) from .types import ( + ActionItem, + ActionItemsRequest, + ActionItemsResponse, AssemblyAIError, AutohighlightResponse, AutohighlightResult, @@ -145,6 +148,9 @@ def __getattr__(name: str): __all__ = [ # types + "ActionItem", + "ActionItemsRequest", + "ActionItemsResponse", "AssemblyAIError", "AsyncClient", "AsyncDictationTranscriber", diff --git a/assemblyai/__version__.py b/assemblyai/__version__.py index 0a0a43a..38cf6db 100644 --- a/assemblyai/__version__.py +++ b/assemblyai/__version__.py @@ -1 +1 @@ -__version__ = "1.9.0" +__version__ = "1.9.1" diff --git a/assemblyai/types.py b/assemblyai/types.py index cb5ba27..3d0a5c3 100644 --- a/assemblyai/types.py +++ b/assemblyai/types.py @@ -773,6 +773,18 @@ class SummarizationRequest(BaseModel): "How much effort to spend on this task (default: 'low')" +class ActionItemsRequest(BaseModel): + """ + Action items configuration for speech understanding + """ + + include_decisions: Optional[bool] = None + "Also extract key decisions made during the conversation" + + effort: Optional[Literal["low", "medium"]] = None + "How much effort to spend on this task (default: 'low')" + + class SpeechUnderstandingFeatureRequests(BaseModel): """ Speech understanding feature requests @@ -790,6 +802,9 @@ class SpeechUnderstandingFeatureRequests(BaseModel): summarization: Optional[SummarizationRequest] = None "Summarization configuration" + action_items: Optional[ActionItemsRequest] = None + "Action items configuration" + class SpeechUnderstandingRequest(BaseModel): """ @@ -890,6 +905,36 @@ class SummarizationResponse(BaseModel): "The effort level that was used" +class ActionItem(BaseModel): + """ + An action item or decision extracted from the audio + """ + + action_item: str + "The extracted action item or decision" + + quote: Optional[str] = None + "The source quote from the audio" + + timestamp: Optional[int] = None + "Timestamp in milliseconds from audio start" + + +class ActionItemsResponse(BaseModel): + """ + Action items response containing status and extracted items + """ + + status: str + "Status of the action items feature" + + effort: Optional[Literal["low", "medium"]] = None + "The effort level that was used" + + items: Optional[List[ActionItem]] = None + "List of extracted action items" + + class SpeechUnderstandingFeatureResponses(BaseModel): """ Speech understanding feature responses grouped together @@ -907,6 +952,9 @@ class SpeechUnderstandingFeatureResponses(BaseModel): summarization: Optional[SummarizationResponse] = None "Summarization results" + action_items: Optional[ActionItemsResponse] = None + "Action items results including status and items" + class SpeechUnderstandingResponse(BaseModel): """ @@ -1253,7 +1301,7 @@ def __init__( language_detection_options: Options for controlling the behavior or Automatic Language Detection. speech_threshold: Reject audio files that contain less than this fraction of speech. Valid values are in the range [0,1] inclusive. raw_transcription_config: Create the config from a `RawTranscriptionConfig` - speech_understanding: Speech understanding configuration for LLM Gateway features (speaker identification, translation, custom formatting) + speech_understanding: Speech understanding configuration for LLM Gateway features (speaker identification, translation, custom formatting, summarization, action items) temperature: Change how deterministic the response is, with 0 being the most deterministic and 1 being the least deterministic. remove_audio_tags: When set to 'all', removes all bracketed audio/speaker tags from the transcript. Only supported for Universal-3 Pro. keyterms_prompt_options: Options for controlling keyterms boosting behavior when using `keyterms_prompt`. diff --git a/tests/unit/test_action_items.py b/tests/unit/test_action_items.py new file mode 100644 index 0000000..adfa3fe --- /dev/null +++ b/tests/unit/test_action_items.py @@ -0,0 +1,138 @@ +from pytest_httpx import HTTPXMock + +import assemblyai as aai +import tests.unit.factories as factories +import tests.unit.unit_test_utils as test_utils + +aai.settings.api_key = "test" + + +def test_action_items_request_model(): + req = aai.ActionItemsRequest( + effort="low", + include_decisions=True, + ) + assert req.effort == "low" + assert req.include_decisions is True + + +def test_action_items_request_defaults(): + req = aai.ActionItemsRequest() + assert req.effort is None + assert req.include_decisions is None + + +def test_action_items_in_speech_understanding_request(): + req = aai.SpeechUnderstandingRequest( + request=aai.SpeechUnderstandingFeatureRequests( + action_items=aai.ActionItemsRequest( + effort="medium", + include_decisions=False, + ) + ) + ) + assert req.request is not None + assert req.request.action_items is not None + assert req.request.action_items.effort == "medium" + assert req.request.action_items.include_decisions is False + + +def test_action_items_dict_style_in_transcription_config(): + config = aai.TranscriptionConfig( + speech_understanding={ + "request": { + "action_items": { + "effort": "low", + "include_decisions": True, + } + } + } + ) + assert config.speech_understanding is not None + assert config.speech_understanding.request is not None + assert config.speech_understanding.request.action_items is not None + assert config.speech_understanding.request.action_items.effort == "low" + assert config.speech_understanding.request.action_items.include_decisions is True + + +def test_action_items_response_model(): + resp = aai.ActionItemsResponse( + status="success", + effort="low", + items=[ + aai.ActionItem( + action_item="Send follow-up email", + quote="I will send follow-up email", + timestamp=124500, + ) + ], + ) + assert resp.status == "success" + assert resp.effort == "low" + assert resp.items is not None + assert len(resp.items) == 1 + assert resp.items[0].action_item == "Send follow-up email" + assert resp.items[0].quote == "I will send follow-up email" + assert resp.items[0].timestamp == 124500 + + +def test_transcript_speech_understanding_action_items(httpx_mock: HTTPXMock): + mock_response = factories.generate_dict_factory( + factories.TranscriptCompletedResponseFactory + )() + mock_response["speech_understanding"] = { + "request": { + "action_items": { + "effort": "low", + "include_decisions": True, + } + }, + "response": { + "action_items": { + "status": "success", + "effort": "low", + "items": [ + { + "action_item": "Schedule sync meeting", + "quote": "Let's meet tomorrow at 10", + "timestamp": 45000, + } + ], + } + }, + } + + request_body, transcript = test_utils.submit_mock_transcription_request( + httpx_mock, + mock_response, + config=aai.TranscriptionConfig( + speech_understanding={ + "request": { + "action_items": { + "effort": "low", + "include_decisions": True, + } + } + } + ), + ) + + # Check request payload serialization + assert request_body.get("speech_understanding") is not None + su_request = request_body["speech_understanding"]["request"] + assert su_request.get("action_items") is not None + assert su_request["action_items"]["effort"] == "low" + assert su_request["action_items"]["include_decisions"] is True + + # Check parsed transcript response + assert transcript.speech_understanding is not None + assert transcript.speech_understanding.response is not None + assert transcript.speech_understanding.response.action_items is not None + ai_resp = transcript.speech_understanding.response.action_items + assert ai_resp.status == "success" + assert ai_resp.effort == "low" + assert ai_resp.items is not None + assert len(ai_resp.items) == 1 + assert ai_resp.items[0].action_item == "Schedule sync meeting" + assert ai_resp.items[0].quote == "Let's meet tomorrow at 10" + assert ai_resp.items[0].timestamp == 45000