diff --git a/open_notebook/graphs/ask.py b/open_notebook/graphs/ask.py index 51806ba..74f0ae6 100644 --- a/open_notebook/graphs/ask.py +++ b/open_notebook/graphs/ask.py @@ -14,6 +14,7 @@ from open_notebook.domain.notebook import vector_search from open_notebook.exceptions import OpenNotebookError from open_notebook.utils import clean_thinking_content from open_notebook.utils.error_classifier import classify_error +from open_notebook.utils.text_utils import extract_text_content class SubGraphState(TypedDict): @@ -65,11 +66,7 @@ async def call_model_with_messages(state: ThreadState, config: RunnableConfig) - ai_message = await model.ainvoke(system_prompt) # Clean the thinking content from the response - message_content = ( - ai_message.content - if isinstance(ai_message.content, str) - else str(ai_message.content) - ) + message_content = extract_text_content(ai_message.content) cleaned_content = clean_thinking_content(message_content) # Parse the cleaned JSON content @@ -118,11 +115,7 @@ async def provide_answer(state: SubGraphState, config: RunnableConfig) -> dict: max_tokens=2000, ) ai_message = await model.ainvoke(system_prompt) - ai_content = ( - ai_message.content - if isinstance(ai_message.content, str) - else str(ai_message.content) - ) + ai_content = extract_text_content(ai_message.content) return {"answers": [clean_thinking_content(ai_content)]} except OpenNotebookError: raise @@ -141,11 +134,7 @@ async def write_final_answer(state: ThreadState, config: RunnableConfig) -> dict max_tokens=2000, ) ai_message = await model.ainvoke(system_prompt) - final_content = ( - ai_message.content - if isinstance(ai_message.content, str) - else str(ai_message.content) - ) + final_content = extract_text_content(ai_message.content) return {"final_answer": clean_thinking_content(final_content)} except OpenNotebookError: raise diff --git a/open_notebook/graphs/chat.py b/open_notebook/graphs/chat.py index 34469c4..dced398 100644 --- a/open_notebook/graphs/chat.py +++ b/open_notebook/graphs/chat.py @@ -16,6 +16,7 @@ from open_notebook.domain.notebook import Notebook from open_notebook.exceptions import OpenNotebookError from open_notebook.utils import clean_thinking_content from open_notebook.utils.error_classifier import classify_error +from open_notebook.utils.text_utils import extract_text_content class ThreadState(TypedDict): @@ -72,11 +73,7 @@ def call_model_with_messages(state: ThreadState, config: RunnableConfig) -> dict ai_message = model.invoke(payload) # Clean thinking content from AI response (e.g., ... tags) - content = ( - ai_message.content - if isinstance(ai_message.content, str) - else str(ai_message.content) - ) + content = extract_text_content(ai_message.content) cleaned_content = clean_thinking_content(content) cleaned_message = ai_message.model_copy(update={"content": cleaned_content}) diff --git a/open_notebook/graphs/prompt.py b/open_notebook/graphs/prompt.py index 00bc356..cd80d10 100644 --- a/open_notebook/graphs/prompt.py +++ b/open_notebook/graphs/prompt.py @@ -7,7 +7,7 @@ from langgraph.graph import END, START, StateGraph from typing_extensions import TypedDict from open_notebook.ai.provision import provision_langchain_model -from open_notebook.utils.text_utils import clean_thinking_content +from open_notebook.utils.text_utils import clean_thinking_content, extract_text_content class PatternChainState(TypedDict): @@ -33,7 +33,7 @@ async def call_model(state: dict, config: RunnableConfig) -> dict: response = await chain.ainvoke(payload) # Clean thinking tags from response (handles extended thinking models) - output = clean_thinking_content(str(response.content)) + output = clean_thinking_content(extract_text_content(response.content)) return {"output": output} diff --git a/open_notebook/graphs/source_chat.py b/open_notebook/graphs/source_chat.py index 843f605..bccc25e 100644 --- a/open_notebook/graphs/source_chat.py +++ b/open_notebook/graphs/source_chat.py @@ -17,6 +17,7 @@ from open_notebook.exceptions import OpenNotebookError from open_notebook.utils import clean_thinking_content from open_notebook.utils.context_builder import ContextBuilder from open_notebook.utils.error_classifier import classify_error +from open_notebook.utils.text_utils import extract_text_content class SourceChatState(TypedDict): @@ -172,11 +173,7 @@ def _call_model_with_source_context_inner( ai_message = model.invoke(payload) # Clean thinking content from AI response (e.g., ... tags) - content = ( - ai_message.content - if isinstance(ai_message.content, str) - else str(ai_message.content) - ) + content = extract_text_content(ai_message.content) cleaned_content = clean_thinking_content(content) cleaned_message = ai_message.model_copy(update={"content": cleaned_content}) diff --git a/open_notebook/graphs/transformation.py b/open_notebook/graphs/transformation.py index 8639a37..65bc526 100644 --- a/open_notebook/graphs/transformation.py +++ b/open_notebook/graphs/transformation.py @@ -10,6 +10,7 @@ from open_notebook.domain.transformation import DefaultPrompts, Transformation from open_notebook.exceptions import OpenNotebookError from open_notebook.utils import clean_thinking_content from open_notebook.utils.error_classifier import classify_error +from open_notebook.utils.text_utils import extract_text_content class TransformationState(TypedDict): @@ -51,9 +52,7 @@ async def run_transformation(state: dict, config: RunnableConfig) -> dict: response = await chain.ainvoke(payload) # Clean thinking content from the response - response_content = ( - response.content if isinstance(response.content, str) else str(response.content) - ) + response_content = extract_text_content(response.content) cleaned_content = clean_thinking_content(response_content) if source: diff --git a/open_notebook/utils/text_utils.py b/open_notebook/utils/text_utils.py index 3846924..ff7ea14 100644 --- a/open_notebook/utils/text_utils.py +++ b/open_notebook/utils/text_utils.py @@ -117,3 +117,29 @@ def clean_thinking_content(content: str) -> str: """ _, cleaned_content = parse_thinking_content(content) return cleaned_content + + +def extract_text_content(content) -> str: + """Extract text from LLM response content. + + Handles both plain string responses and structured content formats + (e.g. Gemini's envelope format): + [{'type': 'text', 'text': '...', 'extras': {...}}] + + Args: + content: The content from an AI message, either a string or a list of parts. + + Returns: + The extracted text content as a string. + """ + if isinstance(content, str): + return content + if isinstance(content, list): + text_parts = [] + for part in content: + if isinstance(part, dict) and "text" in part: + text_parts.append(part["text"]) + elif isinstance(part, str): + text_parts.append(part) + return "".join(text_parts) + return str(content) diff --git a/pyproject.toml b/pyproject.toml index caa3ade..d41cd14 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ "ai-prompter>=0.3,<1", "esperanto>=2.19.3,<3", "surrealdb>=1.0.4", - "podcast-creator>=0.9.1,<1", + "podcast-creator>=0.9.4,<1", "surreal-commands>=1.3.1,<2", "numpy>=2.4.1", ] diff --git a/uv.lock b/uv.lock index ef336aa..375ad11 100644 --- a/uv.lock +++ b/uv.lock @@ -2168,7 +2168,7 @@ requires-dist = [ { name = "loguru", specifier = ">=0.7.2" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.11.1" }, { name = "numpy", specifier = ">=2.4.1" }, - { name = "podcast-creator", specifier = ">=0.9.1,<1" }, + { name = "podcast-creator", specifier = ">=0.9.4,<1" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.0.1" }, { name = "pydantic", specifier = ">=2.9.2" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, @@ -2519,7 +2519,7 @@ wheels = [ [[package]] name = "podcast-creator" -version = "0.9.1" +version = "0.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ai-prompter" }, @@ -2535,9 +2535,9 @@ dependencies = [ { name = "requests" }, { name = "tiktoken" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/de/f7ee60b502dad23b724d669be31fdeb6a790e306968c2cd6a079388262be/podcast_creator-0.9.1.tar.gz", hash = "sha256:177ae68b18c7efd815e555dcec3c644e541bd053e2c63669fd0a18a008b2f374", size = 470751, upload-time = "2026-02-16T17:58:44.275Z" } +sdist = { url = "https://files.pythonhosted.org/packages/97/4a/9f23b55659d7d236645593a4b75141837ed88568ba6a6a370b01d97827e6/podcast_creator-0.9.4.tar.gz", hash = "sha256:9e40a77c105d0b02f04a3eef7881a34454ef556fabd8297fe68d50307ca5f926", size = 472357, upload-time = "2026-02-17T20:21:57.257Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/d7/687284d059fc490a19d60af8f07a66b19895e15946e7ced143096d3c5ea0/podcast_creator-0.9.1-py3-none-any.whl", hash = "sha256:e3e513f2aacccd96c15bcab891216ff447568551c4392b3f12575aa0cf0cbeee", size = 74421, upload-time = "2026-02-16T17:58:42.818Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ac/b331aae683771964f0574189c8dbc1bc0c7b22aca9a376d61c3248180848/podcast_creator-0.9.4-py3-none-any.whl", hash = "sha256:2bd1138cbd1a4deda9da657e7e2b9c8a7d8c0cc43c649506af4837aeb708d46f", size = 74844, upload-time = "2026-02-17T20:21:58.271Z" }, ] [[package]]