From 48addb496442892c21382ff27d03578b3f9d7ac6 Mon Sep 17 00:00:00 2001 From: Dirk Brand <51947788+dirkbrnd@users.noreply.github.com> Date: Fri, 20 Dec 2024 10:42:50 +0200 Subject: [PATCH 1/4] Add a startup idea validator workflow (#1610) ## Description This adds an example workflow for validating a new startup idea and doing market research. --- cookbook/workflows/startup_idea_validator.py | 213 +++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 cookbook/workflows/startup_idea_validator.py diff --git a/cookbook/workflows/startup_idea_validator.py b/cookbook/workflows/startup_idea_validator.py new file mode 100644 index 000000000..c4070f622 --- /dev/null +++ b/cookbook/workflows/startup_idea_validator.py @@ -0,0 +1,213 @@ +""" +1. Install dependencies using: `pip install openai exa_py sqlalchemy phidata` +2. Run the script using: `python cookbook/workflows/blog_post_generator.py` +""" + +import json +from typing import Optional, Iterator + +from pydantic import BaseModel, Field + +from phi.agent import Agent +from phi.model.openai import OpenAIChat +from phi.tools.googlesearch import GoogleSearch +from phi.workflow import Workflow, RunResponse, RunEvent +from phi.storage.workflow.sqlite import SqlWorkflowStorage +from phi.utils.pprint import pprint_run_response +from phi.utils.log import logger + + +class IdeaClarification(BaseModel): + originality: str = Field(..., description="Originality of the idea.") + mission: str = Field(..., description="Mission of the company.") + objectives: str = Field(..., description="Objectives of the company.") + + +class MarketResearch(BaseModel): + total_addressable_market: str = Field(..., description="Total addressable market (TAM).") + serviceable_available_market: str = Field(..., description="Serviceable available market (SAM).") + serviceable_obtainable_market: str = Field(..., description="Serviceable obtainable market (SOM).") + target_customer_segments: str = Field(..., description="Target customer segments.") + + +class StartupIdeaValidator(Workflow): + idea_clarifier_agent: Agent = Agent( + model=OpenAIChat(id="gpt-4o-mini"), + instructions=[ + "Given a user's startup idea, its your goal to refine that idea. ", + "Evaluates the originality of the idea by comparing it with existing concepts. ", + "Define the mission and objectives of the startup.", + ], + add_history_to_messages=True, + add_datetime_to_instructions=True, + response_model=IdeaClarification, + structured_outputs=True, + debug_mode=False, + ) + + market_research_agent: Agent = Agent( + model=OpenAIChat(id="gpt-4o-mini"), + tools=[GoogleSearch()], + instructions=[ + "You are provided with a startup idea and the company's mission and objectives. ", + "Estimate the total addressable market (TAM), serviceable available market (SAM), and serviceable obtainable market (SOM). ", + "Define target customer segments and their characteristics. ", + "Search the web for resources if you need to.", + ], + add_history_to_messages=True, + add_datetime_to_instructions=True, + response_model=MarketResearch, + structured_outputs=True, + debug_mode=False, + ) + + competitor_analysis_agent: Agent = Agent( + model=OpenAIChat(id="gpt-4o-mini"), + tools=[GoogleSearch()], + instructions=[ + "You are provided with a startup idea and some market research related to the idea. ", + "Identify existing competitors in the market. ", + "Perform Strengths, Weaknesses, Opportunities, and Threats (SWOT) analysis for each competitor. ", + "Assess the startup’s potential positioning relative to competitors.", + ], + add_history_to_messages=True, + add_datetime_to_instructions=True, + markdown=True, + debug_mode=False, + ) + + report_agent: Agent = Agent( + model=OpenAIChat(id="gpt-4o-mini"), + instructions=[ + "You are provided with a startup idea and other data about the idea. ", + "Summarise everything into a single report.", + ], + add_history_to_messages=True, + add_datetime_to_instructions=True, + markdown=True, + debug_mode=False, + ) + + def get_idea_clarification(self, startup_idea: str) -> Optional[IdeaClarification]: + try: + response: RunResponse = self.idea_clarifier_agent.run(startup_idea) + + # Check if we got a valid response + if not response or not response.content: + logger.warning("Empty Idea Clarification response") + # Check if the response is of the expected type + if not isinstance(response.content, IdeaClarification): + logger.warning("Invalid response type") + + return response.content + + except Exception as e: + logger.warning(f"Failed: {str(e)}") + + return None + + def get_market_research(self, startup_idea: str, idea_clarification: IdeaClarification) -> Optional[MarketResearch]: + agent_input = {"startup_idea": startup_idea, **idea_clarification.model_dump()} + + try: + response: RunResponse = self.market_research_agent.run(json.dumps(agent_input, indent=4)) + + # Check if we got a valid response + if not response or not response.content: + logger.warning("Empty Market Research response") + + # Check if the response is of the expected type + if not isinstance(response.content, MarketResearch): + logger.warning("Invalid response type") + + return response.content + + except Exception as e: + logger.warning(f"Failed: {str(e)}") + + return None + + def get_competitor_analysis(self, startup_idea: str, market_research: MarketResearch) -> Optional[str]: + agent_input = {"startup_idea": startup_idea, **market_research.model_dump()} + + try: + response: RunResponse = self.competitor_analysis_agent.run(json.dumps(agent_input, indent=4)) + + # Check if we got a valid response + if not response or not response.content: + logger.warning("Empty Competitor Analysis response") + + return response.content + + except Exception as e: + logger.warning(f"Failed: {str(e)}") + + return None + + def run(self, startup_idea: str) -> Iterator[RunResponse]: + logger.info(f"Generating a startup validation report for: {startup_idea}") + + # Clarify and quantify the idea + idea_clarification: Optional[IdeaClarification] = self.get_idea_clarification(startup_idea) + + if idea_clarification is None: + yield RunResponse( + event=RunEvent.workflow_completed, + content=f"Sorry, could not even clarify the idea: {startup_idea}", + ) + return + + # Do some market research + market_research: Optional[MarketResearch] = self.get_market_research(startup_idea, idea_clarification) + + if market_research is None: + yield RunResponse( + event=RunEvent.workflow_completed, + content="Market research failed", + ) + return + + competitor_analysis: Optional[str] = self.get_competitor_analysis(startup_idea, market_research) + + # Compile the final report + final_response: RunResponse = self.report_agent.run( + json.dumps( + { + "startup_idea": startup_idea, + **idea_clarification.model_dump(), + **market_research.model_dump(), + "competitor_analysis_report": competitor_analysis, + }, + indent=4, + ) + ) + + yield RunResponse(content=final_response.content, event=RunEvent.workflow_completed) + + +# Run the workflow if the script is executed directly +if __name__ == "__main__": + from rich.prompt import Prompt + + # Get idea from user + idea = Prompt.ask( + "[bold]What is your startup idea?[/bold]\n✨", + default="A marketplace for Christmas Ornaments made from leather", + ) + + # Convert the idea to a URL-safe string for use in session_id + url_safe_idea = idea.lower().replace(" ", "-") + + startup_idea_validator = StartupIdeaValidator( + description="Startup Idea Validator", + session_id=f"validate-startup-idea-{url_safe_idea}", + storage=SqlWorkflowStorage( + table_name="validate_startup_ideas_workflow", + db_file="tmp/workflows.db", + ), + debug_mode=True + ) + + final_report: Iterator[RunResponse] = startup_idea_validator.run(startup_idea=idea) + + pprint_run_response(final_report, markdown=True) From 3a7077b48affd448730a230e1095592880f90cae Mon Sep 17 00:00:00 2001 From: Manthan Gupta <42516515+manthanguptaa@users.noreply.github.com> Date: Fri, 20 Dec 2024 17:29:40 +0530 Subject: [PATCH 2/4] Feat/continue workflow playground (#1552) ## Description 1. Adds ability to continue a workflow run 2. Fixes getting default value of run function param when the type is of FieldInfo class --------- Co-authored-by: Ashpreet Bedi Co-authored-by: Dirk Brand <51947788+dirkbrnd@users.noreply.github.com> --- phi/playground/router.py | 7 ++++++- phi/playground/schemas.py | 1 + phi/workflow/workflow.py | 4 +++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/phi/playground/router.py b/phi/playground/router.py index 0cf81bdba..5023755bb 100644 --- a/phi/playground/router.py +++ b/phi/playground/router.py @@ -553,8 +553,13 @@ async def run_workflow(workflow_id: str, body: WorkflowRunRequest): if workflow is None: raise HTTPException(status_code=404, detail="Workflow not found") + if body.session_id is not None: + logger.debug(f"Continuing session: {body.session_id}") + else: + logger.debug("Creating new session") + # Create a new instance of this workflow - new_workflow_instance = workflow.deep_copy(update={"workflow_id": workflow_id}) + new_workflow_instance = workflow.deep_copy(update={"workflow_id": workflow_id, "session_id": body.session_id}) new_workflow_instance.user_id = body.user_id # Return based on the response type diff --git a/phi/playground/schemas.py b/phi/playground/schemas.py index 7eb470ea4..660e210a1 100644 --- a/phi/playground/schemas.py +++ b/phi/playground/schemas.py @@ -68,3 +68,4 @@ class WorkflowRenameRequest(BaseModel): class WorkflowRunRequest(BaseModel): input: Dict[str, Any] user_id: Optional[str] = None + session_id: Optional[str] = None diff --git a/phi/workflow/workflow.py b/phi/workflow/workflow.py index 668930813..dccab8dac 100644 --- a/phi/workflow/workflow.py +++ b/phi/workflow/workflow.py @@ -321,7 +321,9 @@ def __init__(self, **data): self._run_parameters = { name: { "name": name, - "default": param.default if param.default is not inspect.Parameter.empty else None, + "default": param.default.default + if hasattr(param.default, "__class__") and param.default.__class__.__name__ == "FieldInfo" + else (param.default if param.default is not inspect.Parameter.empty else None), "annotation": ( param.annotation.__name__ if hasattr(param.annotation, "__name__") From b65c7a73f3f98549f30d140114eb345d1e7b0eb4 Mon Sep 17 00:00:00 2001 From: Dirk Brand <51947788+dirkbrnd@users.noreply.github.com> Date: Fri, 20 Dec 2024 16:50:58 +0200 Subject: [PATCH 3/4] Release 2.7.5 (#1617) ## Description This update introduces the Confluence tool for collaboration, improves tool compatibility, and resolves deep copy issues for Ollama chat agents. **New Features:** - Confluence Tool: Added a new tool using the Atlassian Confluence SDK, enabling operations such as listing pages in a space, creating and updating pages, retrieving page content by title, and getting space details. **Improvements:** - Tool Compatibility: Enhanced older custom functions with manually specified descriptions and parameters to align with the updated tool-building system. **Bug Fixes:** - Deep Copy for Ollama Chat Agents: Addressed an issue where manually set clients caused errors during agent model copying, ensuring all properties are properly handled. --------- Co-authored-by: Anurag --- cookbook/agents/14_generate_image.py | 8 ++------ cookbook/chunking/__init__.py | 0 cookbook/mysql-init/init.sql | 16 ++++++++++++++++ cookbook/providers/ollama/agent_set_client.py | 18 ++++++++++++++++++ cookbook/readers/__init__.py | 0 cookbook/run_mysql.sh | 10 ++++++++++ cookbook/tools/composio_tools.py | 1 - phi/model/ollama/chat.py | 4 ++++ phi/tools/function.py | 13 ++++++++++--- pyproject.toml | 2 +- 10 files changed, 61 insertions(+), 11 deletions(-) create mode 100644 cookbook/chunking/__init__.py create mode 100644 cookbook/mysql-init/init.sql create mode 100644 cookbook/providers/ollama/agent_set_client.py create mode 100644 cookbook/readers/__init__.py create mode 100755 cookbook/run_mysql.sh diff --git a/cookbook/agents/14_generate_image.py b/cookbook/agents/14_generate_image.py index 1f8323155..642d62710 100644 --- a/cookbook/agents/14_generate_image.py +++ b/cookbook/agents/14_generate_image.py @@ -16,9 +16,5 @@ images = image_agent.get_images() if images and isinstance(images, list): for image_response in images: - image_data = image_response.get("data") # type: ignore - if image_data: - for image in image_data: - image_url = image.get("url") # type: ignore - if image_url: - print(image_url) + image_url = image_response.url + print(image_url) diff --git a/cookbook/chunking/__init__.py b/cookbook/chunking/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cookbook/mysql-init/init.sql b/cookbook/mysql-init/init.sql new file mode 100644 index 000000000..398c886a0 --- /dev/null +++ b/cookbook/mysql-init/init.sql @@ -0,0 +1,16 @@ +-- Create 'users' table +CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NOT NULL UNIQUE, + email VARCHAR(100) NOT NULL UNIQUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Create 'products' table +CREATE TABLE IF NOT EXISTS products ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + description TEXT, + price DECIMAL(10,2) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); diff --git a/cookbook/providers/ollama/agent_set_client.py b/cookbook/providers/ollama/agent_set_client.py new file mode 100644 index 000000000..3377811e6 --- /dev/null +++ b/cookbook/providers/ollama/agent_set_client.py @@ -0,0 +1,18 @@ +"""Run `pip install yfinance` to install dependencies.""" + +from ollama import Client as OllamaClient +from phi.agent import Agent, RunResponse # noqa +from phi.model.ollama import Ollama +from phi.playground import Playground, serve_playground_app +from phi.tools.yfinance import YFinanceTools + +agent = Agent( + model=Ollama(id="llama3.1:8b", client=OllamaClient()), + tools=[YFinanceTools(stock_price=True)], + markdown=True, +) + +app = Playground(agents=[agent]).get_app() + +if __name__ == "__main__": + serve_playground_app("agent_set_client:app", reload=True) diff --git a/cookbook/readers/__init__.py b/cookbook/readers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cookbook/run_mysql.sh b/cookbook/run_mysql.sh new file mode 100755 index 000000000..908347566 --- /dev/null +++ b/cookbook/run_mysql.sh @@ -0,0 +1,10 @@ +docker run -d \ + -e MYSQL_ROOT_PASSWORD=phi \ + -e MYSQL_DATABASE=phi \ + -e MYSQL_USER=phi \ + -e MYSQL_PASSWORD=phi \ + -p 3306:3306 \ + -v mysql_data:/var/lib/mysql \ + -v $(pwd)/cookbook/mysql-init:/docker-entrypoint-initdb.d \ + --name mysql \ + mysql:8.0 diff --git a/cookbook/tools/composio_tools.py b/cookbook/tools/composio_tools.py index 69650892b..9aa8575dc 100644 --- a/cookbook/tools/composio_tools.py +++ b/cookbook/tools/composio_tools.py @@ -1,7 +1,6 @@ from phi.agent import Agent from composio_phidata import Action, ComposioToolSet # type: ignore - toolset = ComposioToolSet() composio_tools = toolset.get_tools(actions=[Action.GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER]) diff --git a/phi/model/ollama/chat.py b/phi/model/ollama/chat.py index acc744454..51f39089e 100644 --- a/phi/model/ollama/chat.py +++ b/phi/model/ollama/chat.py @@ -722,3 +722,7 @@ async def aresponse_stream(self, messages: List[Message]) -> Any: async for post_tool_call_response in self.ahandle_post_tool_call_messages_stream(messages=messages): yield post_tool_call_response logger.debug("---------- Ollama Async Response End ----------") + + def model_copy(self, *, update: Optional[dict[str, Any]] = None, deep: bool = False) -> "Ollama": + new_model = Ollama(**self.model_dump(exclude={"client"}), client=self.client) + return new_model diff --git a/phi/tools/function.py b/phi/tools/function.py index a8174e147..452fad808 100644 --- a/phi/tools/function.py +++ b/phi/tools/function.py @@ -53,7 +53,7 @@ class Function(BaseModel): # The parameters the functions accepts, described as a JSON Schema object. # To describe a function that accepts no parameters, provide the value {"type": "object", "properties": {}}. parameters: Dict[str, Any] = Field( - default_factory=lambda: {"type": "object", "properties": {}}, + default_factory=lambda: {"type": "object", "properties": {}, "required": []}, description="JSON Schema object describing function parameters", ) strict: Optional[bool] = None @@ -139,6 +139,12 @@ def process_entrypoint(self, strict: bool = False): return parameters = {"type": "object", "properties": {}, "required": []} + + params_set_by_user = False + # If the user set the parameters (i.e. they are different from the default), we should keep them + if self.parameters != parameters: + params_set_by_user = True + try: sig = signature(self.entrypoint) type_hints = get_type_hints(self.entrypoint) @@ -174,8 +180,9 @@ def process_entrypoint(self, strict: bool = False): except Exception as e: logger.warning(f"Could not parse args for {self.name}: {e}", exc_info=True) - self.description = getdoc(self.entrypoint) or self.description - self.parameters = parameters + self.description = self.description or getdoc(self.entrypoint) + if not params_set_by_user: + self.parameters = parameters self.entrypoint = validate_call(self.entrypoint) def get_type_name(self, t: Type[T]): diff --git a/pyproject.toml b/pyproject.toml index 1ca8e856e..f59e309b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "phidata" -version = "2.7.4" +version = "2.7.5" description = "Build multi-modal Agents with memory, knowledge and tools." requires-python = ">=3.7" readme = "README.md" From a4cf57f48eae54946a3b1a63b52e471603774f54 Mon Sep 17 00:00:00 2001 From: Yash Pratap Solanky <101447028+ysolanky@users.noreply.github.com> Date: Thu, 26 Dec 2024 10:27:00 -0500 Subject: [PATCH 4/4] OpenAI audio agent (#1631) ## Description Added OpenAI audio agent cookbook example ## Type of change Please check the options that are relevant: - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Model update - [ ] Infrastructure change ## Checklist - [ ] My code follows Phidata's style guidelines and best practices - [ ] I have performed a self-review of my code - [ ] I have added docstrings and comments for complex logic - [ ] My changes generate no new warnings or errors - [ ] I have added cookbook examples for my new addition (if needed) - [ ] I have updated requirements.txt/pyproject.toml (if needed) - [ ] I have verified my changes in a clean environment --- cookbook/providers/openai/.gitignore | 6 +++++ .../providers/openai/audio_input_agent.py | 18 +++++++++++++ .../providers/openai/audio_output_agent.py | 25 +++++++++++++++++++ cookbook/workflows/startup_idea_validator.py | 2 +- phi/document/reader/csv_reader.py | 2 +- phi/llm/ollama/hermes.py | 2 +- phi/llm/ollama/tools.py | 2 +- 7 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 cookbook/providers/openai/.gitignore create mode 100644 cookbook/providers/openai/audio_input_agent.py create mode 100644 cookbook/providers/openai/audio_output_agent.py diff --git a/cookbook/providers/openai/.gitignore b/cookbook/providers/openai/.gitignore new file mode 100644 index 000000000..525cad7b9 --- /dev/null +++ b/cookbook/providers/openai/.gitignore @@ -0,0 +1,6 @@ +*.jpg +*.png +*.mp3 +*.wav +*.mp4 +*.mp3 diff --git a/cookbook/providers/openai/audio_input_agent.py b/cookbook/providers/openai/audio_input_agent.py new file mode 100644 index 000000000..7c43863f8 --- /dev/null +++ b/cookbook/providers/openai/audio_input_agent.py @@ -0,0 +1,18 @@ +import base64 +import requests +from phi.agent import Agent, RunResponse # noqa +from phi.model.openai import OpenAIChat + +# Fetch the audio file and convert it to a base64 encoded string +url = "https://openaiassets.blob.core.windows.net/$web/API/docs/audio/alloy.wav" +response = requests.get(url) +response.raise_for_status() +wav_data = response.content +encoded_string = base64.b64encode(wav_data).decode("utf-8") + +# Provide the agent with the audio file and get result as text +agent = Agent( + model=OpenAIChat(id="gpt-4o-audio-preview", modalities=["text"]), + markdown=True, +) +agent.print_response("What is in this audio?", audio={"data": encoded_string, "format": "wav"}) diff --git a/cookbook/providers/openai/audio_output_agent.py b/cookbook/providers/openai/audio_output_agent.py new file mode 100644 index 000000000..f08dae193 --- /dev/null +++ b/cookbook/providers/openai/audio_output_agent.py @@ -0,0 +1,25 @@ +import base64 +import requests +from phi.agent import Agent, RunResponse # noqa +from phi.model.openai import OpenAIChat +from phi.utils.audio import write_audio_to_file + +# Fetch the audio file and convert it to a base64 encoded string +url = "https://openaiassets.blob.core.windows.net/$web/API/docs/audio/alloy.wav" +response = requests.get(url) +response.raise_for_status() +wav_data = response.content +encoded_string = base64.b64encode(wav_data).decode("utf-8") + +# Provide the agent with the audio file and audio configuration and get result as text + audio +agent = Agent( + model=OpenAIChat( + id="gpt-4o-audio-preview", modalities=["text", "audio"], audio={"voice": "alloy", "format": "wav"} + ), + markdown=True, +) +agent.print_response("What is in this audio?", audio={"data": encoded_string, "format": "wav"}) + +# Save the response audio to a file +if agent.run_response.response_audio is not None and "data" in agent.run_response.response_audio: + write_audio_to_file(audio=agent.run_response.response_audio["data"], filename="tmp/dog.wav") diff --git a/cookbook/workflows/startup_idea_validator.py b/cookbook/workflows/startup_idea_validator.py index c4070f622..99bc89dfc 100644 --- a/cookbook/workflows/startup_idea_validator.py +++ b/cookbook/workflows/startup_idea_validator.py @@ -205,7 +205,7 @@ def run(self, startup_idea: str) -> Iterator[RunResponse]: table_name="validate_startup_ideas_workflow", db_file="tmp/workflows.db", ), - debug_mode=True + debug_mode=True, ) final_report: Iterator[RunResponse] = startup_idea_validator.run(startup_idea=idea) diff --git a/phi/document/reader/csv_reader.py b/phi/document/reader/csv_reader.py index f9ed770d3..5274007b9 100644 --- a/phi/document/reader/csv_reader.py +++ b/phi/document/reader/csv_reader.py @@ -26,7 +26,7 @@ def read(self, file: Union[Path, IO[Any]], delimiter: str = ",", quotechar: str else: logger.info(f"Reading uploaded file: {file.name}") file.seek(0) - file_content = io.StringIO(file.read().decode("utf-8")) + file_content = io.StringIO(file.read().decode("utf-8")) # type: ignore csv_name = Path(file.name).stem if isinstance(file, Path) else file.name.split(".")[0] csv_content = "" diff --git a/phi/llm/ollama/hermes.py b/phi/llm/ollama/hermes.py index aa5b78647..3dd6cf17d 100644 --- a/phi/llm/ollama/hermes.py +++ b/phi/llm/ollama/hermes.py @@ -258,7 +258,7 @@ def response_stream(self, messages: List[Message]) -> Iterator[str]: # logger.info(f"Ollama partial response: {response}") # logger.info(f"Ollama partial response type: {type(response)}") response_message: Optional[dict] = response.get("message") - response_content = response_message.get("content") if response_message else None + response_content: str = response_message.get("content", "") if response_message else "" # logger.info(f"Ollama partial response content: {response_content}") # Add response content to assistant message diff --git a/phi/llm/ollama/tools.py b/phi/llm/ollama/tools.py index 6c7ef683e..9d0072bd1 100644 --- a/phi/llm/ollama/tools.py +++ b/phi/llm/ollama/tools.py @@ -259,7 +259,7 @@ def response_stream(self, messages: List[Message]) -> Iterator[str]: # logger.info(f"Ollama partial response: {response}") # logger.info(f"Ollama partial response type: {type(response)}") response_message: Optional[dict] = response.get("message") - response_content = response_message.get("content") if response_message else None + response_content: str = response_message.get("content", "") if response_message else "" # logger.info(f"Ollama partial response content: {response_content}") # Add response content to assistant message