* fix(notebooks): correct SUMMARIZE response parsing for get_description/get_summary The VfAZjd (SUMMARIZE) RPC returns a triple-nested structure: result = [[[summary_string], [[topics]], ...]] The previous parsing assumed: - Summary at result[0][0] (a string) — actually a list - Topics at result[1][0] — actually at result[0][1][0] This caused get_description() to always return an empty summary and no suggested topics, showing "No summary available" in the CLI. Fixed parsing to use: - Summary: result[0][0][0] - Topics: result[0][1][0] Updated all affected unit/integration tests to use the correct response structure (confirmed against the real API cassette in notebooks_get_summary.yaml). Fixes #147 * test(e2e): strengthen summary assertions to catch empty results * refactor(notebooks): simplify SUMMARIZE response parsing per review feedback * docs(rpc-reference): fix SUMMARIZE response structure (triple-nested)
This commit is contained in:
parent
7939795fa2
commit
6a6e1a9484
5 changed files with 76 additions and 51 deletions
|
|
@ -928,11 +928,15 @@ await rpc_call(
|
|||
|
||||
# Response structure:
|
||||
# [
|
||||
# [summary_text], # [0][0]: Summary string
|
||||
# [[ # [1][0]: Suggested topics array
|
||||
# [question, prompt], # Each topic has question and prompt
|
||||
# ...
|
||||
# ]],
|
||||
# [ # [0]: Outer container
|
||||
# [summary_text], # [0][0]: Summary wrapped in list; text at [0][0][0]
|
||||
# [[ # [0][1][0]: Suggested topics array
|
||||
# [question, prompt], # Each topic has question and prompt
|
||||
# ...
|
||||
# ]],
|
||||
# null, null, null,
|
||||
# [[question, score], ...], # [0][5]: Topics with relevance scores
|
||||
# ]
|
||||
# ]
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -135,8 +135,14 @@ class NotebooksAPI:
|
|||
params,
|
||||
source_path=f"/notebook/{notebook_id}",
|
||||
)
|
||||
if result and isinstance(result, list) and len(result) > 0:
|
||||
return str(result[0]) if result[0] else ""
|
||||
# Response structure: [[[summary_string, ...], topics, ...]]
|
||||
# Summary is at result[0][0][0]
|
||||
try:
|
||||
if result and isinstance(result, list):
|
||||
summary = result[0][0][0]
|
||||
return str(summary) if summary else ""
|
||||
except (IndexError, TypeError):
|
||||
pass
|
||||
return ""
|
||||
|
||||
async def get_description(self, notebook_id: str) -> NotebookDescription:
|
||||
|
|
@ -168,22 +174,30 @@ class NotebooksAPI:
|
|||
summary = ""
|
||||
suggested_topics: list[SuggestedTopic] = []
|
||||
|
||||
# Response structure: [[[summary_string], [[topics]], ...]]
|
||||
# Summary is at result[0][0][0], topics at result[0][1][0]
|
||||
if result and isinstance(result, list):
|
||||
# Summary at [0][0]
|
||||
if len(result) > 0 and isinstance(result[0], list) and len(result[0]) > 0:
|
||||
summary = result[0][0] if isinstance(result[0][0], str) else ""
|
||||
try:
|
||||
outer = result[0]
|
||||
|
||||
# Suggested topics at [1][0]
|
||||
if len(result) > 1 and isinstance(result[1], list) and len(result[1]) > 0:
|
||||
topics_list = result[1][0] if isinstance(result[1][0], list) else []
|
||||
for topic in topics_list:
|
||||
if isinstance(topic, list) and len(topic) >= 2:
|
||||
suggested_topics.append(
|
||||
SuggestedTopic(
|
||||
question=topic[0] if isinstance(topic[0], str) else "",
|
||||
prompt=topic[1] if isinstance(topic[1], str) else "",
|
||||
# Summary at outer[0][0]
|
||||
summary_val = outer[0][0]
|
||||
summary = str(summary_val) if summary_val else ""
|
||||
|
||||
# Suggested topics at outer[1][0]
|
||||
topics_list = outer[1][0]
|
||||
if isinstance(topics_list, list):
|
||||
for topic in topics_list:
|
||||
if isinstance(topic, list) and len(topic) >= 2:
|
||||
suggested_topics.append(
|
||||
SuggestedTopic(
|
||||
question=str(topic[0]) if topic[0] else "",
|
||||
prompt=str(topic[1]) if topic[1] else "",
|
||||
)
|
||||
)
|
||||
)
|
||||
except (IndexError, TypeError):
|
||||
# A partial result (e.g. summary but no topics) is possible.
|
||||
pass
|
||||
|
||||
return NotebookDescription(summary=summary, suggested_topics=suggested_topics)
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ class TestNotebookDescription:
|
|||
description = await client.notebooks.get_description(read_only_notebook_id)
|
||||
|
||||
assert isinstance(description, NotebookDescription)
|
||||
assert description.summary is not None
|
||||
assert description.summary, "Expected non-empty summary from get_description"
|
||||
assert isinstance(description.suggested_topics, list)
|
||||
|
||||
|
||||
|
|
@ -92,8 +92,7 @@ class TestNotebookSummary:
|
|||
async def test_get_summary(self, client, read_only_notebook_id):
|
||||
"""Test getting notebook summary."""
|
||||
summary = await client.notebooks.get_summary(read_only_notebook_id)
|
||||
# Summary may be empty string if not generated yet
|
||||
assert isinstance(summary, str)
|
||||
assert summary, "Expected non-empty summary from get_summary"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.readonly
|
||||
|
|
|
|||
|
|
@ -201,7 +201,9 @@ class TestSummary:
|
|||
httpx_mock: HTTPXMock,
|
||||
build_rpc_response,
|
||||
):
|
||||
response = build_rpc_response(RPCMethod.SUMMARIZE, ["Summary of the notebook content..."])
|
||||
response = build_rpc_response(
|
||||
RPCMethod.SUMMARIZE, [[["Summary of the notebook content..."]]]
|
||||
)
|
||||
httpx_mock.add_response(content=response.encode())
|
||||
|
||||
async with NotebookLMClient(auth_tokens) as client:
|
||||
|
|
@ -313,7 +315,7 @@ class TestNotebooksAPIAdditional:
|
|||
"""Test getting notebook summary."""
|
||||
response = build_rpc_response(
|
||||
RPCMethod.SUMMARIZE,
|
||||
["This is a comprehensive summary of the notebook content..."],
|
||||
[[["This is a comprehensive summary of the notebook content..."]]],
|
||||
)
|
||||
httpx_mock.add_response(content=response.encode())
|
||||
|
||||
|
|
@ -372,13 +374,15 @@ class TestNotebooksAPIAdditional:
|
|||
response = build_rpc_response(
|
||||
RPCMethod.SUMMARIZE,
|
||||
[
|
||||
["This notebook covers AI research."],
|
||||
[
|
||||
["This notebook covers AI research."],
|
||||
[
|
||||
["What are the main findings?", "Explain the key findings"],
|
||||
["How was the study conducted?", "Describe methodology"],
|
||||
]
|
||||
],
|
||||
[
|
||||
["What are the main findings?", "Explain the key findings"],
|
||||
["How was the study conducted?", "Describe methodology"],
|
||||
]
|
||||
],
|
||||
]
|
||||
],
|
||||
)
|
||||
httpx_mock.add_response(content=response.encode())
|
||||
|
|
@ -522,7 +526,7 @@ class TestNotebookEdgeCases:
|
|||
"""Test getting description with no suggested topics."""
|
||||
response = build_rpc_response(
|
||||
RPCMethod.SUMMARIZE,
|
||||
[["Summary text"], []],
|
||||
[[["Summary text"], []]],
|
||||
)
|
||||
httpx_mock.add_response(content=response.encode())
|
||||
|
||||
|
|
@ -543,14 +547,16 @@ class TestNotebookEdgeCases:
|
|||
response = build_rpc_response(
|
||||
RPCMethod.SUMMARIZE,
|
||||
[
|
||||
["Summary"],
|
||||
[
|
||||
["Summary"],
|
||||
[
|
||||
["Valid question", "Valid prompt"],
|
||||
["Only question"], # Missing prompt
|
||||
"not a list", # Not a list
|
||||
]
|
||||
],
|
||||
[
|
||||
["Valid question", "Valid prompt"],
|
||||
["Only question"], # Missing prompt
|
||||
"not a list", # Not a list
|
||||
]
|
||||
],
|
||||
]
|
||||
],
|
||||
)
|
||||
httpx_mock.add_response(content=response.encode())
|
||||
|
|
@ -574,11 +580,11 @@ class TestDescribeEdgeCases:
|
|||
httpx_mock: HTTPXMock,
|
||||
build_rpc_response,
|
||||
):
|
||||
"""Line 171->188: result has only [0] (no result[1]) so topics stay empty."""
|
||||
# result = [["A summary"]] — len is 1, so result[1] branch is never entered
|
||||
"""result has only outer[0] (no outer[1]) so topics stay empty."""
|
||||
# result = [[["A summary"]]] — outer[0] has summary, no outer[1] for topics
|
||||
response = build_rpc_response(
|
||||
RPCMethod.SUMMARIZE,
|
||||
[["A summary"]],
|
||||
[[["A summary"]]],
|
||||
)
|
||||
httpx_mock.add_response(content=response.encode())
|
||||
|
||||
|
|
@ -595,11 +601,11 @@ class TestDescribeEdgeCases:
|
|||
httpx_mock: HTTPXMock,
|
||||
build_rpc_response,
|
||||
):
|
||||
"""Line 173->177: result[1] exists but is an empty list, so inner block skipped."""
|
||||
# result = [["A summary"], []] — result[1] has len 0, so the inner if is false
|
||||
"""outer[1] exists but is an empty list, so topics block is skipped."""
|
||||
# result = [[["A summary"], []]] — outer[1] is empty, so topics are skipped
|
||||
response = build_rpc_response(
|
||||
RPCMethod.SUMMARIZE,
|
||||
[["A summary"], []],
|
||||
[[["A summary"], []]],
|
||||
)
|
||||
httpx_mock.add_response(content=response.encode())
|
||||
|
||||
|
|
@ -616,11 +622,11 @@ class TestDescribeEdgeCases:
|
|||
httpx_mock: HTTPXMock,
|
||||
build_rpc_response,
|
||||
):
|
||||
"""Line 173->177: result[1] is present but not a list, inner block skipped."""
|
||||
# result = [["A summary"], "not-a-list"]
|
||||
"""outer[1] is present but not a list, so topics block is skipped."""
|
||||
# result = [[["A summary"], "not-a-list"]] — outer[1] is not a list, topics skipped
|
||||
response = build_rpc_response(
|
||||
RPCMethod.SUMMARIZE,
|
||||
[["A summary"], "not-a-list"],
|
||||
[[["A summary"], "not-a-list"]],
|
||||
)
|
||||
httpx_mock.add_response(content=response.encode())
|
||||
|
||||
|
|
|
|||
|
|
@ -254,13 +254,15 @@ class TestGetNotebookDescription:
|
|||
async def test_get_notebook_description_parses_response(self, mock_client):
|
||||
"""Test get_notebook_description parses full response."""
|
||||
mock_response = [
|
||||
["This notebook explores **AI** and **machine learning**."],
|
||||
[
|
||||
["This notebook explores **AI** and **machine learning**."],
|
||||
[
|
||||
["What is the future of AI?", "Create a detailed briefing..."],
|
||||
["How does ML work?", "Explain the fundamentals..."],
|
||||
]
|
||||
],
|
||||
[
|
||||
["What is the future of AI?", "Create a detailed briefing..."],
|
||||
["How does ML work?", "Explain the fundamentals..."],
|
||||
]
|
||||
],
|
||||
]
|
||||
]
|
||||
mock_client._core.rpc_call = AsyncMock(return_value=mock_response)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue