From a3dea3155ada133da25008123a9829c8fd3b7a27 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 27 May 2026 08:09:50 +0000 Subject: [PATCH 1/2] fix(api): parse structured research output models Co-authored-by: william --- src/linkup/_client.py | 44 +++++++++++++++++++++++++++++---- tests/unit/client_test.py | 51 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 6 deletions(-) diff --git a/src/linkup/_client.py b/src/linkup/_client.py index 0547be5..053061c 100644 --- a/src/linkup/_client.py +++ b/src/linkup/_client.py @@ -358,7 +358,9 @@ def research( timeout=timeout, ) - return self._parse_research_task(response.json()) + return self._parse_research_task( + response.json(), structured_output_schema=structured_output_schema + ) async def async_research( self, @@ -427,7 +429,9 @@ async def async_research( timeout=timeout, ) - return self._parse_research_task(response.json()) + return self._parse_research_task( + response.json(), structured_output_schema=structured_output_schema + ) def list_research( self, @@ -1456,12 +1460,42 @@ def _parse_fetch_response(self, response: httpx.Response) -> LinkupFetchResponse def _parse_fetch_response_data(self, response_data: Any) -> LinkupFetchResponse: # noqa: ANN401 return LinkupFetchResponse.model_validate(response_data) - def _parse_research_task(self, task_data: dict[str, Any]) -> LinkupResearchTask: + def _parse_research_output_data( + self, + response_data: Any, # noqa: ANN401 + output_type: Literal["sourcedAnswer", "structured"], + structured_output_schema: type[BaseModel] | dict[str, Any] | str | None, + ) -> Any: # noqa: ANN401 + if output_type == "sourcedAnswer": + return LinkupSourcedAnswer.model_validate(response_data) + if output_type == "structured": + if structured_output_schema is None: + raise ValueError( + "structured_output_schema must be provided when output_type is 'structured'" + ) + if isinstance(structured_output_schema, type) and issubclass( + structured_output_schema, BaseModel + ): + return structured_output_schema.model_validate(response_data) + return response_data + raise ValueError(f"Unexpected output_type value: '{output_type}'") + + def _parse_research_task( + self, + task_data: dict[str, Any], + structured_output_schema: type[BaseModel] | dict[str, Any] | str | None = None, + ) -> LinkupResearchTask: research_input = LinkupResearchTaskInput.model_validate(task_data["input"]) parsed_output = task_data.get("output") - if parsed_output is not None and research_input.output_type == "sourcedAnswer": - parsed_output = LinkupSourcedAnswer.model_validate(parsed_output) + if parsed_output is not None: + parsed_output = self._parse_research_output_data( + response_data=parsed_output, + output_type=research_input.output_type, + structured_output_schema=structured_output_schema + if structured_output_schema is not None + else research_input.structured_output_schema, + ) return LinkupResearchTask.model_validate( {**task_data, "input": research_input, "output": parsed_output} diff --git a/tests/unit/client_test.py b/tests/unit/client_test.py index 92941f3..85c1950 100644 --- a/tests/unit/client_test.py +++ b/tests/unit/client_test.py @@ -638,6 +638,55 @@ def test_research(mocker: MockerFixture, client: linkup.Client) -> None: ) +def test_research_structured_output_model(mocker: MockerFixture, client: linkup.Client) -> None: + request_mock = mocker.patch( + "httpx.Client.request", + return_value=Response( + status_code=200, + content=b""" + { + "createdAt": "2026-05-18T00:00:00.000Z", + "error": null, + "id": "bfeb26f5-f4d6-47d2-9818-7f62fbcd0b0c", + "input": { + "outputType": "structured", + "q": "query", + "structuredOutputSchema": { + "type": "object" + } + }, + "output": { + "summary": "done" + }, + "status": "completed", + "type": "research", + "updatedAt": "2026-05-18T00:00:00.000Z" + } + """, + ), + ) + + research_response = client.research( + query="query", + output_type="structured", + structured_output_schema=Company, + ) + + request_mock.assert_called_once_with( + method="POST", + url="/research", + json={ + "q": "query", + "outputType": "structured", + "structuredOutputSchema": json.dumps(Company.model_json_schema()), + }, + timeout=None, + ) + assert research_response.output == Company(summary="done") + assert research_response.input.query == "query" + assert research_response.input.structured_output_schema == {"type": "object"} + + @pytest.mark.asyncio async def test_async_research(mocker: MockerFixture, client: linkup.Client) -> None: request_mock = mocker.patch( @@ -683,7 +732,7 @@ async def test_async_research(mocker: MockerFixture, client: linkup.Client) -> N }, timeout=None, ) - assert research_response.output == {"summary": "done"} + assert research_response.output == Company(summary="done") assert research_response.input.query == "query" assert research_response.input.structured_output_schema == {"type": "object"} From e74e5d7f2d01d856813d38609006bb49b7caa292 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 27 May 2026 08:12:00 +0000 Subject: [PATCH 2/2] test(api): use research summary model fixture Co-authored-by: william --- tests/unit/client_test.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/unit/client_test.py b/tests/unit/client_test.py index 85c1950..c21ca16 100644 --- a/tests/unit/client_test.py +++ b/tests/unit/client_test.py @@ -19,6 +19,10 @@ class Company(BaseModel): founders_names: list[str] +class ResearchSummary(BaseModel): + summary: str + + test_search_parameters = [ ( {"query": "query", "depth": "standard", "output_type": "searchResults"}, @@ -669,7 +673,7 @@ def test_research_structured_output_model(mocker: MockerFixture, client: linkup. research_response = client.research( query="query", output_type="structured", - structured_output_schema=Company, + structured_output_schema=ResearchSummary, ) request_mock.assert_called_once_with( @@ -678,11 +682,11 @@ def test_research_structured_output_model(mocker: MockerFixture, client: linkup. json={ "q": "query", "outputType": "structured", - "structuredOutputSchema": json.dumps(Company.model_json_schema()), + "structuredOutputSchema": json.dumps(ResearchSummary.model_json_schema()), }, timeout=None, ) - assert research_response.output == Company(summary="done") + assert research_response.output == ResearchSummary(summary="done") assert research_response.input.query == "query" assert research_response.input.structured_output_schema == {"type": "object"} @@ -719,7 +723,7 @@ async def test_async_research(mocker: MockerFixture, client: linkup.Client) -> N research_response = await client.async_research( query="query", output_type="structured", - structured_output_schema=Company, + structured_output_schema=ResearchSummary, ) request_mock.assert_called_once_with( @@ -728,11 +732,11 @@ async def test_async_research(mocker: MockerFixture, client: linkup.Client) -> N json={ "q": "query", "outputType": "structured", - "structuredOutputSchema": json.dumps(Company.model_json_schema()), + "structuredOutputSchema": json.dumps(ResearchSummary.model_json_schema()), }, timeout=None, ) - assert research_response.output == Company(summary="done") + assert research_response.output == ResearchSummary(summary="done") assert research_response.input.query == "query" assert research_response.input.structured_output_schema == {"type": "object"}