From 63d60121ea50de19053adf67096cfbe12d693ee8 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 17 Dec 2024 17:36:21 -0300 Subject: [PATCH 01/15] feat: Implement agent components for context management and action routing --- .../components/agents/agent_action_router.py | 55 +++++++++ .../components/agents/agent_context.py | 27 +++++ .../components/agents/decide_action.py | 40 +++++++ .../components/agents/execute_action.py | 35 ++++++ .../components/agents/generate_thought.py | 39 +++++++ .../langflow/components/agents/new_agent.py | 105 ++++++++++++++++++ .../components/agents/write_final_answer.py | 34 ++++++ 7 files changed, 335 insertions(+) create mode 100644 src/backend/base/langflow/components/agents/agent_action_router.py create mode 100644 src/backend/base/langflow/components/agents/agent_context.py create mode 100644 src/backend/base/langflow/components/agents/decide_action.py create mode 100644 src/backend/base/langflow/components/agents/execute_action.py create mode 100644 src/backend/base/langflow/components/agents/generate_thought.py create mode 100644 src/backend/base/langflow/components/agents/new_agent.py create mode 100644 src/backend/base/langflow/components/agents/write_final_answer.py diff --git a/src/backend/base/langflow/components/agents/agent_action_router.py b/src/backend/base/langflow/components/agents/agent_action_router.py new file mode 100644 index 000000000000..b0e5ccd8f605 --- /dev/null +++ b/src/backend/base/langflow/components/agents/agent_action_router.py @@ -0,0 +1,55 @@ +from langchain.schema.agent import AgentFinish + +from langflow.base.agents.context import AgentContext +from langflow.custom import Component +from langflow.io import HandleInput, IntInput, Output + + +class AgentActionRouter(Component): + display_name = "Agent Action Router" + description = "Routes the agent's flow based on the last action type." + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__iteration_updated = False + + inputs = [ + HandleInput(name="agent_context", display_name="Agent Context", input_types=["AgentContext"], required=True), + IntInput(name="max_iterations", display_name="Max Interations", required=True, value=5), + ] + + outputs = [ + Output(name="execute_tool", display_name="Execute Tool", method="route_to_execute_tool", cache=False), + Output(name="final_answer", display_name="Final Answer", method="route_to_final_answer", cache=False), + ] + + def _pre_run_setup(self): + self.__iteration_updated = False + + def _get_context_message_and_route_to_stop(self) -> tuple[str, str]: + if ( + isinstance(self.agent_context.last_action, AgentFinish) + or self.agent_context.iteration >= self.agent_context.max_iterations + ): + return "Provide Final Answer", "execute_tool" + return "Execute Tool", "final_answer" + + def iterate_and_stop_once(self, route_to_stop: str): + if not self.__iteration_updated: + self.agent_context.iteration += 1 + self.__iteration_updated = True + self.stop(route_to_stop) + + def route_to_execute_tool(self) -> AgentContext: + context_message, route_to_stop = self._get_context_message_and_route_to_stop() + self.agent_context.update_context("Router Decision", context_message) + self.iterate_and_stop_once(route_to_stop) + self.status = self.agent_context.to_data_repr() + return self.agent_context + + def route_to_final_answer(self) -> AgentContext: + context_message, route_to_stop = self._get_context_message_and_route_to_stop() + self.agent_context.update_context("Router Decision", context_message) + self.iterate_and_stop_once(route_to_stop) + self.status = self.agent_context.to_data_repr() + return self.agent_context diff --git a/src/backend/base/langflow/components/agents/agent_context.py b/src/backend/base/langflow/components/agents/agent_context.py new file mode 100644 index 000000000000..0ec44fe27a8b --- /dev/null +++ b/src/backend/base/langflow/components/agents/agent_context.py @@ -0,0 +1,27 @@ +from langflow.base.agents.context import AgentContext +from langflow.custom import Component +from langflow.io import HandleInput, IntInput, MessageTextInput, Output + + +class AgentContextBuilder(Component): + display_name = "Agent Context Builder" + description = "Builds the AgentContext instance for the agent execution loop." + + inputs = [ + HandleInput(name="tools", display_name="Tools", input_types=["Tool"], is_list=True, required=True), + HandleInput(name="llm", display_name="Language Model", input_types=["LanguageModel"], required=True), + MessageTextInput(name="initial_context", display_name="Initial Context", required=False), + IntInput(name="max_iterations", display_name="Max Iterations", value=5, required=False), + ] + + outputs = [Output(name="agent_context", display_name="Agent Context", method="build_context")] + + def build_context(self) -> AgentContext: + tools = [self.tools] if self.tools and not isinstance(self.tools, list) else self.tools + + tools_dict = {tool.name: tool for tool in tools} + context = AgentContext(tools=tools_dict, llm=self.llm, context=self.initial_context or "", iteration=0) + if self.max_iterations is not None: + context.max_iterations = self.max_iterations + self.status = context.to_data_repr() + return context diff --git a/src/backend/base/langflow/components/agents/decide_action.py b/src/backend/base/langflow/components/agents/decide_action.py new file mode 100644 index 000000000000..a51c8a9380af --- /dev/null +++ b/src/backend/base/langflow/components/agents/decide_action.py @@ -0,0 +1,40 @@ +from typing import TYPE_CHECKING + +from langchain.agents.output_parsers.tools import parse_ai_message_to_tool_action + +from langflow.base.agents.context import AgentContext +from langflow.custom import Component +from langflow.io import HandleInput, MessageTextInput, Output + +if TYPE_CHECKING: + from langchain_core.messages import AIMessage + + +class DecideActionComponent(Component): + display_name = "Decide Action" + description = "Decides on an action based on the current thought and context." + + inputs = [ + HandleInput(name="agent_context", display_name="Agent Context", input_types=["AgentContext"], required=True), + MessageTextInput( + name="prompt", + display_name="Prompt", + required=True, + value="Based on your thought, decide the best action to take next.", + ), + ] + + outputs = [Output(name="processed_agent_context", display_name="Agent Context", method="decide_action")] + + def decide_action(self) -> AgentContext: + # Append the prompt after the accumulated context following ReAct format + full_prompt = f"{self.agent_context.get_full_context()}\n{self.prompt}\nAction:" + response: AIMessage = self.agent_context.llm.invoke(full_prompt) + action = parse_ai_message_to_tool_action(response) + if isinstance(action, list): + self.agent_context.last_action = action[0] + else: + self.agent_context.last_action = action + self.agent_context.update_context("Action", action) + self.status = self.agent_context.to_data_repr() + return self.agent_context diff --git a/src/backend/base/langflow/components/agents/execute_action.py b/src/backend/base/langflow/components/agents/execute_action.py new file mode 100644 index 000000000000..be0bf0deadc7 --- /dev/null +++ b/src/backend/base/langflow/components/agents/execute_action.py @@ -0,0 +1,35 @@ +from typing import TYPE_CHECKING + +from langflow.custom import Component +from langflow.io import HandleInput, Output +from langflow.schema.message import Message + +if TYPE_CHECKING: + from langchain_core.agents import AgentAction + + +class ExecuteActionComponent(Component): + display_name = "Execute Action" + description = "Executes the decided action using available tools." + + inputs = [ + HandleInput(name="agent_context", display_name="Agent Context", input_types=["AgentContext"], required=True), + ] + + outputs = [Output(name="action_execution", display_name="Agent Context", method="execute_action")] + + def execute_action(self) -> Message: + action: AgentAction = self.agent_context.last_action + + tools = self.agent_context.tools + if action.tool in tools: + data = tools[action.tool](action.tool_input) + self.agent_context.last_action_result = data + self.agent_context.update_context("Action Result", data) + else: + error_msg = f"Error: Action '{action}' not found in available tools." + self.agent_context.last_action_result = error_msg + self.agent_context.update_context("Action Result", error_msg) + tool_call_result = f"Tool: {action.tool} called with input: {action.tool_input} and returned: {data}" + self.status = self.agent_context.to_data_repr() + return Message(text=tool_call_result) diff --git a/src/backend/base/langflow/components/agents/generate_thought.py b/src/backend/base/langflow/components/agents/generate_thought.py new file mode 100644 index 000000000000..47b7428ac307 --- /dev/null +++ b/src/backend/base/langflow/components/agents/generate_thought.py @@ -0,0 +1,39 @@ +from typing import TYPE_CHECKING + +from langflow.base.agents.context import AgentContext +from langflow.custom import Component +from langflow.io import HandleInput, MessageTextInput, Output + +if TYPE_CHECKING: + from langchain_core.messages import AIMessage + + +class GenerateThoughtComponent(Component): + display_name = "Generate Thought" + description = "Generates a thought based on the current context." + + inputs = [ + HandleInput( + name="agent_context", + display_name="Agent Context", + input_types=["AgentContext"], + required=True, + ), + MessageTextInput( + name="prompt", + display_name="Prompt", + required=True, + value="Based on the provided context, generate your next thought.", + ), + ] + + outputs = [Output(name="processed_agent_context", display_name="Agent Context", method="generate_thought")] + + def generate_thought(self) -> AgentContext: + # Append the prompt after the accumulated context following ReAct format + full_prompt = f"{self.agent_context.get_full_context()}\n{self.prompt}\nThought:" + thought: AIMessage = self.agent_context.llm.invoke(full_prompt) + self.agent_context.thought = thought + self.agent_context.update_context("Thought", thought) + self.status = self.agent_context.to_data_repr() + return self.agent_context diff --git a/src/backend/base/langflow/components/agents/new_agent.py b/src/backend/base/langflow/components/agents/new_agent.py new file mode 100644 index 000000000000..6a3c3680ae0a --- /dev/null +++ b/src/backend/base/langflow/components/agents/new_agent.py @@ -0,0 +1,105 @@ +from loguru import logger + +from langflow.components.agents.agent_action_router import AgentActionRouter +from langflow.components.agents.agent_context import AgentContextBuilder +from langflow.components.agents.decide_action import DecideActionComponent +from langflow.components.agents.execute_action import ExecuteActionComponent +from langflow.components.agents.generate_thought import GenerateThoughtComponent +from langflow.components.agents.write_final_answer import ProvideFinalAnswerComponent +from langflow.components.inputs.chat import ChatInput +from langflow.components.outputs import ChatOutput +from langflow.components.prompts import PromptComponent +from langflow.custom import Component +from langflow.graph.graph.base import Graph +from langflow.graph.state.model import create_state_model +from langflow.io import BoolInput, HandleInput, IntInput, MessageTextInput, MultilineInput, Output +from langflow.schema.message import Message + + +class LangflowAgent(Component): + display_name = "Langflow Agent" + description = "Customizable Agent component" + + inputs = [ + HandleInput(name="llm", display_name="Language Model", input_types=["LanguageModel"], required=True), + HandleInput(name="tools", display_name="Tools", input_types=["Tool"], is_list=True, required=True), + IntInput(name="max_iterations", display_name="Max Iterations", value=5), + BoolInput(name="verbose", display_name="Verbose", value=False), + MultilineInput(name="system_prompt", display_name="System Prompt", value="You are a helpful assistant."), + MultilineInput(name="user_prompt", display_name="User Prompt", value="{input}"), + MultilineInput( + name="loop_prompt", + display_name="Loop Prompt", + value="Last Action Result: {last_action_result}\nBased on the actions taken, here's the final answer:", + ), + MessageTextInput( + name="decide_action_prompt", + display_name="Decide Action Prompt", + value="Based on your thought, decide the best action to take next.", + advanced=True, + ), + MessageTextInput( + name="final_answer_prompt", + display_name="Final Answer Prompt", + value="Considering all observations, provide the final answer to the user's query.", + advanced=True, + ), + ] + outputs = [Output(name="response", display_name="Response", method="get_response")] + + async def get_response(self) -> Message: + # Chat input initialization + chat_input = ChatInput().set(input_value=self.user_prompt) + + # Agent Context Builder + agent_context = AgentContextBuilder().set( + initial_context=chat_input.message_response, + tools=self.tools, + llm=self.llm, + max_iterations=self.max_iterations, + ) + + # Generate Thought + generate_thought = GenerateThoughtComponent().set( + agent_context=agent_context.build_context, + ) + + # Decide Action + decide_action = DecideActionComponent().set( + agent_context=generate_thought.generate_thought, + prompt=self.decide_action_prompt, + ) + + # Agent Action Router + action_router = AgentActionRouter().set( + agent_context=decide_action.decide_action, + max_iterations=self.max_iterations, + ) + + # Execute Action + execute_action = ExecuteActionComponent().set(agent_context=action_router.route_to_execute_tool) + # Loop Prompt + loop_prompt = PromptComponent().set( + template=self.loop_prompt, + answer=execute_action.execute_action, + ) + + generate_thought.set(prompt=loop_prompt.build_prompt) + + # Final Answer + final_answer = ProvideFinalAnswerComponent().set( + agent_context=action_router.route_to_final_answer, + prompt=self.final_answer_prompt, + ) + + # Chat output + chat_output = ChatOutput().set(input_value=final_answer.get_final_answer) + output_model = create_state_model("AgentOutput", output=chat_output.message_response) + + # Build the graph + graph = Graph(chat_input, chat_output) + async for result in graph.async_start(max_iterations=self.max_iterations): + if self.verbose: + logger.info(result) + + return output_model.output diff --git a/src/backend/base/langflow/components/agents/write_final_answer.py b/src/backend/base/langflow/components/agents/write_final_answer.py new file mode 100644 index 000000000000..2c7b9d33c668 --- /dev/null +++ b/src/backend/base/langflow/components/agents/write_final_answer.py @@ -0,0 +1,34 @@ +from typing import TYPE_CHECKING + +from langflow.custom import Component +from langflow.io import HandleInput, MessageTextInput, Output +from langflow.schema.message import Message + +if TYPE_CHECKING: + from langchain_core.messages import AIMessage + + +class ProvideFinalAnswerComponent(Component): + display_name = "Provide Final Answer" + description = "Generates the final answer based on the agent's context." + + inputs = [ + HandleInput(name="agent_context", display_name="Agent Context", input_types=["AgentContext"], required=True), + MessageTextInput( + name="prompt", + display_name="Prompt", + required=True, + value="Considering all observations, provide the final answer to the user's query.", + ), + ] + + outputs = [Output(name="final_answer", method="get_final_answer")] + + def get_final_answer(self) -> Message: + # Append the prompt after the accumulated context following ReAct format + full_prompt = f"{self.agent_context.get_full_context()}\n{self.prompt}\nFinal Answer:" + final_answer: AIMessage = self.agent_context.llm.invoke(full_prompt) + self.agent_context.final_answer = final_answer + self.agent_context.update_context("Final Answer", final_answer) + self.status = self.agent_context.to_data_repr() + return Message(text=final_answer.content) From 4a832e73e1edf6cbc46023978f11748bbe07bc5d Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 17 Dec 2024 18:13:54 -0300 Subject: [PATCH 02/15] fix: Include initial context at the end of the full context output --- src/backend/base/langflow/base/agents/context.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/backend/base/langflow/base/agents/context.py b/src/backend/base/langflow/base/agents/context.py index 8e4961ecc579..7a4da8c1f759 100644 --- a/src/backend/base/langflow/base/agents/context.py +++ b/src/backend/base/langflow/base/agents/context.py @@ -96,7 +96,8 @@ def _serialize_context_history_tuple(self, context_history_tuple: tuple[str, str return f"{name}: {value}" def get_full_context(self) -> str: - context_history_reversed = self.context_history[::-1] + initial_context = self.context_history[0][1] + context_history_reversed = self.context_history[1:][::-1] context_formatted = "\n".join( [ self._serialize_context_history_tuple(context_history_tuple) @@ -106,4 +107,7 @@ def get_full_context(self) -> str: return f""" Context: {context_formatted} + +Initial Context: +{initial_context} """ From b59f9feb98bbecc5bd063cb705d0f8d4f2d23d6e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 17 Dec 2024 18:14:02 -0300 Subject: [PATCH 03/15] fix: Update action logging in DecideActionComponent to store action log --- src/backend/base/langflow/components/agents/decide_action.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backend/base/langflow/components/agents/decide_action.py b/src/backend/base/langflow/components/agents/decide_action.py index a51c8a9380af..b58b4c641509 100644 --- a/src/backend/base/langflow/components/agents/decide_action.py +++ b/src/backend/base/langflow/components/agents/decide_action.py @@ -33,8 +33,9 @@ def decide_action(self) -> AgentContext: action = parse_ai_message_to_tool_action(response) if isinstance(action, list): self.agent_context.last_action = action[0] + action = action[0] else: self.agent_context.last_action = action - self.agent_context.update_context("Action", action) + self.agent_context.update_context("Action", action.log) self.status = self.agent_context.to_data_repr() return self.agent_context From 870f0506613398649b8a78274120157c7ada2c6c Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 17 Dec 2024 18:16:16 -0300 Subject: [PATCH 04/15] fix: Handle invalid LLM responses in GenerateThoughtComponent by raising an error when no thought is generated --- .../langflow/components/agents/generate_thought.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/backend/base/langflow/components/agents/generate_thought.py b/src/backend/base/langflow/components/agents/generate_thought.py index 47b7428ac307..91d6eb45d954 100644 --- a/src/backend/base/langflow/components/agents/generate_thought.py +++ b/src/backend/base/langflow/components/agents/generate_thought.py @@ -1,5 +1,7 @@ from typing import TYPE_CHECKING +from langchain.agents.output_parsers.tools import parse_ai_message_to_tool_action + from langflow.base.agents.context import AgentContext from langflow.custom import Component from langflow.io import HandleInput, MessageTextInput, Output @@ -33,6 +35,15 @@ def generate_thought(self) -> AgentContext: # Append the prompt after the accumulated context following ReAct format full_prompt = f"{self.agent_context.get_full_context()}\n{self.prompt}\nThought:" thought: AIMessage = self.agent_context.llm.invoke(full_prompt) + if not thought.content: + action = parse_ai_message_to_tool_action(thought) + if action: + msg = ( + "Invalid LLM response: An action was returned but no thought was generated. " + "The LLM should first generate a thought explaining its reasoning before taking any action. " + "Please check the prompt and LLM configuration. Maybe use a better model." + ) + raise ValueError(msg) self.agent_context.thought = thought self.agent_context.update_context("Thought", thought) self.status = self.agent_context.to_data_repr() From d77849db44ea65146894c75d7cd5c7a7558183bc Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Tue, 17 Dec 2024 18:16:23 -0300 Subject: [PATCH 05/15] fix: Correct output model instantiation in LangflowAgent to ensure proper state management --- src/backend/base/langflow/components/agents/new_agent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/base/langflow/components/agents/new_agent.py b/src/backend/base/langflow/components/agents/new_agent.py index 6a3c3680ae0a..0907d931b1e4 100644 --- a/src/backend/base/langflow/components/agents/new_agent.py +++ b/src/backend/base/langflow/components/agents/new_agent.py @@ -94,8 +94,8 @@ async def get_response(self) -> Message: # Chat output chat_output = ChatOutput().set(input_value=final_answer.get_final_answer) - output_model = create_state_model("AgentOutput", output=chat_output.message_response) - + agent_output_model = create_state_model("AgentOutput", output=chat_output.message_response) + output_model = agent_output_model() # Build the graph graph = Graph(chat_input, chat_output) async for result in graph.async_start(max_iterations=self.max_iterations): From 5d0f1435f9dfb5c3217193d0dcee5612c5046fe0 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 18 Dec 2024 14:16:10 -0300 Subject: [PATCH 06/15] test: Add unit tests for ChatInput component to verify attribute independence --- .../test_component_instance_attributes.py | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/backend/tests/unit/custom/component/test_component_instance_attributes.py diff --git a/src/backend/tests/unit/custom/component/test_component_instance_attributes.py b/src/backend/tests/unit/custom/component/test_component_instance_attributes.py new file mode 100644 index 000000000000..0dcbc5a18ee8 --- /dev/null +++ b/src/backend/tests/unit/custom/component/test_component_instance_attributes.py @@ -0,0 +1,118 @@ +import pytest +from langflow.components.inputs.chat import ChatInput +from langflow.schema.message import Message + + +@pytest.fixture +def chat_input_instances(): + """Create two instances of ChatInput for testing.""" + chat1 = ChatInput() + chat2 = ChatInput() + return chat1, chat2 + + +def test_input_value_independence(chat_input_instances): + """Test that input_value is independent between instances.""" + chat1, chat2 = chat_input_instances + + # Set different input values + chat1.build(input_value="Hello from chat1") + chat2.build(input_value="Hello from chat2") + + # Verify values are different + assert chat1.input_value != chat2.input_value + assert chat1.input_value == "Hello from chat1" + assert chat2.input_value == "Hello from chat2" + + +def test_sender_name_independence(chat_input_instances): + """Test that sender_name is independent between instances.""" + chat1, chat2 = chat_input_instances + + # Set different sender names + chat1.build(sender_name="Alice") + chat2.build(sender_name="Bob") + + # Verify values are different + assert chat1.sender_name != chat2.sender_name + assert chat1.sender_name == "Alice" + assert chat2.sender_name == "Bob" + + +def test_multiple_attributes_independence(chat_input_instances): + """Test that multiple attributes are independent between instances.""" + chat1, chat2 = chat_input_instances + + # Set multiple attributes for chat1 + chat1.build(input_value="Message 1", sender_name="Alice", background_color="blue", text_color="white") + + # Set different attributes for chat2 + chat2.build(input_value="Message 2", sender_name="Bob", background_color="red", text_color="black") + + # Verify all attributes are independent + assert chat1.input_value != chat2.input_value + assert chat1.sender_name != chat2.sender_name + assert chat1.background_color != chat2.background_color + assert chat1.text_color != chat2.text_color + + +async def test_message_output_independence(chat_input_instances): + """Test that message outputs are independent between instances.""" + chat1, chat2 = chat_input_instances + + # Configure different messages + chat1.build( + input_value="Hello from chat1", + sender_name="Alice", + should_store_message=False, # Prevent actual message storage + ) + chat2.build( + input_value="Hello from chat2", + sender_name="Bob", + should_store_message=False, # Prevent actual message storage + ) + + # Get messages from both instances + message1 = await chat1.message_response() + message2 = await chat2.message_response() + + # Verify messages are different + assert isinstance(message1, Message) + assert isinstance(message2, Message) + assert message1.text != message2.text + assert message1.sender_name != message2.sender_name + + +async def test_status_independence(chat_input_instances): + """Test that status attribute is independent between instances.""" + chat1, chat2 = chat_input_instances + + # Configure and run messages + chat1.build(input_value="Status test 1", sender_name="Alice", should_store_message=False) + chat2.build(input_value="Status test 2", sender_name="Bob", should_store_message=False) + + # Generate messages to update status + await chat1.message_response() + await chat2.message_response() + + # Verify status values are different + assert chat1.status != chat2.status + assert chat1.status.text == "Status test 1" + assert chat2.status.text == "Status test 2" + + +def test_files_independence(chat_input_instances): + """Test that files attribute is independent between instances.""" + chat1, chat2 = chat_input_instances + + # Set different files + files1 = ["file1.txt", "file2.txt"] + files2 = ["file3.txt", "file4.txt"] + + chat1.build(files=files1) + chat2.build(files=files2) + + # Verify files are independent + assert chat1.files != chat2.files + assert chat1.files == files1 + assert chat2.files == files2 From 50654452fde27d9535d63cfc16bd9ce81e60515f Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 18 Dec 2024 14:18:47 -0300 Subject: [PATCH 07/15] Revert "test: Add unit tests for ChatInput component to verify attribute independence" This reverts commit 5d0f1435f9dfb5c3217193d0dcee5612c5046fe0. --- .../test_component_instance_attributes.py | 118 ------------------ 1 file changed, 118 deletions(-) delete mode 100644 src/backend/tests/unit/custom/component/test_component_instance_attributes.py diff --git a/src/backend/tests/unit/custom/component/test_component_instance_attributes.py b/src/backend/tests/unit/custom/component/test_component_instance_attributes.py deleted file mode 100644 index 0dcbc5a18ee8..000000000000 --- a/src/backend/tests/unit/custom/component/test_component_instance_attributes.py +++ /dev/null @@ -1,118 +0,0 @@ -import pytest -from langflow.components.inputs.chat import ChatInput -from langflow.schema.message import Message - - -@pytest.fixture -def chat_input_instances(): - """Create two instances of ChatInput for testing.""" - chat1 = ChatInput() - chat2 = ChatInput() - return chat1, chat2 - - -def test_input_value_independence(chat_input_instances): - """Test that input_value is independent between instances.""" - chat1, chat2 = chat_input_instances - - # Set different input values - chat1.build(input_value="Hello from chat1") - chat2.build(input_value="Hello from chat2") - - # Verify values are different - assert chat1.input_value != chat2.input_value - assert chat1.input_value == "Hello from chat1" - assert chat2.input_value == "Hello from chat2" - - -def test_sender_name_independence(chat_input_instances): - """Test that sender_name is independent between instances.""" - chat1, chat2 = chat_input_instances - - # Set different sender names - chat1.build(sender_name="Alice") - chat2.build(sender_name="Bob") - - # Verify values are different - assert chat1.sender_name != chat2.sender_name - assert chat1.sender_name == "Alice" - assert chat2.sender_name == "Bob" - - -def test_multiple_attributes_independence(chat_input_instances): - """Test that multiple attributes are independent between instances.""" - chat1, chat2 = chat_input_instances - - # Set multiple attributes for chat1 - chat1.build(input_value="Message 1", sender_name="Alice", background_color="blue", text_color="white") - - # Set different attributes for chat2 - chat2.build(input_value="Message 2", sender_name="Bob", background_color="red", text_color="black") - - # Verify all attributes are independent - assert chat1.input_value != chat2.input_value - assert chat1.sender_name != chat2.sender_name - assert chat1.background_color != chat2.background_color - assert chat1.text_color != chat2.text_color - - -async def test_message_output_independence(chat_input_instances): - """Test that message outputs are independent between instances.""" - chat1, chat2 = chat_input_instances - - # Configure different messages - chat1.build( - input_value="Hello from chat1", - sender_name="Alice", - should_store_message=False, # Prevent actual message storage - ) - chat2.build( - input_value="Hello from chat2", - sender_name="Bob", - should_store_message=False, # Prevent actual message storage - ) - - # Get messages from both instances - message1 = await chat1.message_response() - message2 = await chat2.message_response() - - # Verify messages are different - assert isinstance(message1, Message) - assert isinstance(message2, Message) - assert message1.text != message2.text - assert message1.sender_name != message2.sender_name - - -async def test_status_independence(chat_input_instances): - """Test that status attribute is independent between instances.""" - chat1, chat2 = chat_input_instances - - # Configure and run messages - chat1.build(input_value="Status test 1", sender_name="Alice", should_store_message=False) - chat2.build(input_value="Status test 2", sender_name="Bob", should_store_message=False) - - # Generate messages to update status - await chat1.message_response() - await chat2.message_response() - - # Verify status values are different - assert chat1.status != chat2.status - assert chat1.status.text == "Status test 1" - assert chat2.status.text == "Status test 2" - - -def test_files_independence(chat_input_instances): - """Test that files attribute is independent between instances.""" - chat1, chat2 = chat_input_instances - - # Set different files - files1 = ["file1.txt", "file2.txt"] - files2 = ["file3.txt", "file4.txt"] - - chat1.build(files=files1) - chat2.build(files=files2) - - # Verify files are independent - assert chat1.files != chat2.files - assert chat1.files == files1 - assert chat2.files == files2 From 772e425a48d6b0abcf67d6208cb317b1275d4b72 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 18 Dec 2024 09:27:17 -0800 Subject: [PATCH 08/15] fix typo in max iterations input Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../base/langflow/components/agents/agent_action_router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/base/langflow/components/agents/agent_action_router.py b/src/backend/base/langflow/components/agents/agent_action_router.py index b0e5ccd8f605..49c59119f939 100644 --- a/src/backend/base/langflow/components/agents/agent_action_router.py +++ b/src/backend/base/langflow/components/agents/agent_action_router.py @@ -15,7 +15,7 @@ def __init__(self, *args, **kwargs): inputs = [ HandleInput(name="agent_context", display_name="Agent Context", input_types=["AgentContext"], required=True), - IntInput(name="max_iterations", display_name="Max Interations", required=True, value=5), + IntInput(name="max_iterations", display_name="Max Iterations", required=True, value=5), ] outputs = [ From e1711b03109ed687dd9fbff234c8ee82b252597e Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 18 Dec 2024 14:31:47 -0300 Subject: [PATCH 09/15] fix: Improve error messaging in ExecuteActionComponent for better clarity - Updated error handling to provide a more descriptive message when an action is not found in available tools. - The error message now includes the action name directly, enhancing the context for debugging and user feedback. --- src/backend/base/langflow/components/agents/execute_action.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backend/base/langflow/components/agents/execute_action.py b/src/backend/base/langflow/components/agents/execute_action.py index be0bf0deadc7..b4a8dc786a0b 100644 --- a/src/backend/base/langflow/components/agents/execute_action.py +++ b/src/backend/base/langflow/components/agents/execute_action.py @@ -27,7 +27,8 @@ def execute_action(self) -> Message: self.agent_context.last_action_result = data self.agent_context.update_context("Action Result", data) else: - error_msg = f"Error: Action '{action}' not found in available tools." + data = f"Action '{action}' not found in available tools." + error_msg = f"Error: {data}" self.agent_context.last_action_result = error_msg self.agent_context.update_context("Action Result", error_msg) tool_call_result = f"Tool: {action.tool} called with input: {action.tool_input} and returned: {data}" From 3ce89e8c8edf27f62f673995a1b5090a39ff7f13 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 20 Dec 2024 15:09:13 -0300 Subject: [PATCH 10/15] feat: Refactor GenerateThoughtComponent to enhance context formatting and output structure --- .../components/agents/generate_thought.py | 70 ++++++++++++++----- 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/src/backend/base/langflow/components/agents/generate_thought.py b/src/backend/base/langflow/components/agents/generate_thought.py index 91d6eb45d954..fd59831fc73e 100644 --- a/src/backend/base/langflow/components/agents/generate_thought.py +++ b/src/backend/base/langflow/components/agents/generate_thought.py @@ -2,9 +2,10 @@ from langchain.agents.output_parsers.tools import parse_ai_message_to_tool_action -from langflow.base.agents.context import AgentContext from langflow.custom import Component -from langflow.io import HandleInput, MessageTextInput, Output +from langflow.io import MessageTextInput, Output +from langflow.schema.data import Data +from langflow.schema.message import Message if TYPE_CHECKING: from langchain_core.messages import AIMessage @@ -15,12 +16,6 @@ class GenerateThoughtComponent(Component): description = "Generates a thought based on the current context." inputs = [ - HandleInput( - name="agent_context", - display_name="Agent Context", - input_types=["AgentContext"], - required=True, - ), MessageTextInput( name="prompt", display_name="Prompt", @@ -29,12 +24,38 @@ class GenerateThoughtComponent(Component): ), ] - outputs = [Output(name="processed_agent_context", display_name="Agent Context", method="generate_thought")] + outputs = [Output(name="thought", display_name="Generated Thought", method="generate_thought")] + + def _format_context(self) -> str: + ctx = self.ctx + context_parts = [] + + # Add router decision if exists + if "router_decision" in ctx: + context_parts.append(f"Decision: {ctx['router_decision']}") + + # Add thought if exists + if ctx.get("thought"): + context_parts.append(f"Previous Thought: {ctx['thought']}") + + # Add last action and result if they exist + if ctx.get("last_action"): + context_parts.append(f"Last Action: {ctx['last_action']}") + if ctx.get("last_action_result"): + context_parts.append(f"Action Result: {ctx['last_action_result']}") + + # Add iteration info + context_parts.append(f"Current Iteration: {ctx.get('iteration', 0)}/{ctx.get('max_iterations', 5)}") + + return "\n".join(context_parts) + + def generate_thought(self) -> Message: + # Format the full context + full_prompt = f"{self._format_context()}\n{self.prompt}\nThought:" + + # Generate thought using LLM + thought: AIMessage = self.ctx["llm"].invoke(full_prompt) - def generate_thought(self) -> AgentContext: - # Append the prompt after the accumulated context following ReAct format - full_prompt = f"{self.agent_context.get_full_context()}\n{self.prompt}\nThought:" - thought: AIMessage = self.agent_context.llm.invoke(full_prompt) if not thought.content: action = parse_ai_message_to_tool_action(thought) if action: @@ -44,7 +65,22 @@ def generate_thought(self) -> AgentContext: "Please check the prompt and LLM configuration. Maybe use a better model." ) raise ValueError(msg) - self.agent_context.thought = thought - self.agent_context.update_context("Thought", thought) - self.status = self.agent_context.to_data_repr() - return self.agent_context + + # Update context with new thought using update_ctx + self.update_ctx({"thought": thought.content}) + + # Create status data + self.status = [ + Data( + name="Generated Thought", + value=f""" +Context Used: +{self._format_context()} + +New Thought: +{thought.content} +""", + ) + ] + + return Message(text=thought.content) From ca39572ad13d5ac706903448abb429d09ed054ad Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 20 Dec 2024 15:09:21 -0300 Subject: [PATCH 11/15] refactor: Simplify AgentActionRouter by removing AgentContext dependency and enhancing context management --- .../components/agents/agent_action_router.py | 58 ++++++++++++++----- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/src/backend/base/langflow/components/agents/agent_action_router.py b/src/backend/base/langflow/components/agents/agent_action_router.py index 49c59119f939..f5910aca1964 100644 --- a/src/backend/base/langflow/components/agents/agent_action_router.py +++ b/src/backend/base/langflow/components/agents/agent_action_router.py @@ -1,8 +1,9 @@ from langchain.schema.agent import AgentFinish -from langflow.base.agents.context import AgentContext from langflow.custom import Component -from langflow.io import HandleInput, IntInput, Output +from langflow.io import IntInput, Output +from langflow.schema.data import Data +from langflow.schema.message import Message class AgentActionRouter(Component): @@ -14,7 +15,6 @@ def __init__(self, *args, **kwargs): self.__iteration_updated = False inputs = [ - HandleInput(name="agent_context", display_name="Agent Context", input_types=["AgentContext"], required=True), IntInput(name="max_iterations", display_name="Max Iterations", required=True, value=5), ] @@ -25,31 +25,59 @@ def __init__(self, *args, **kwargs): def _pre_run_setup(self): self.__iteration_updated = False + # Initialize context if not already set + if "iteration" not in self.ctx: + self.update_ctx( + { + "iteration": 0, + "max_iterations": self.max_iterations, + "thought": "", + "last_action": None, + "last_action_result": None, + "final_answer": "", + } + ) def _get_context_message_and_route_to_stop(self) -> tuple[str, str]: - if ( - isinstance(self.agent_context.last_action, AgentFinish) - or self.agent_context.iteration >= self.agent_context.max_iterations + ctx = self.ctx + if isinstance(ctx.get("last_action"), AgentFinish) or ctx.get("iteration", 0) >= ctx.get( + "max_iterations", self.max_iterations ): return "Provide Final Answer", "execute_tool" return "Execute Tool", "final_answer" def iterate_and_stop_once(self, route_to_stop: str): if not self.__iteration_updated: - self.agent_context.iteration += 1 + current_iteration = self.ctx.get("iteration", 0) + self.update_ctx({"iteration": current_iteration + 1}) self.__iteration_updated = True self.stop(route_to_stop) - def route_to_execute_tool(self) -> AgentContext: + def _create_status_data(self) -> list[Data]: + ctx = self.ctx + return [ + Data( + name="Agent State", + value=f""" +Iteration: {ctx.get('iteration', 0)} +Last Action: {ctx.get('last_action')} +Last Result: {ctx.get('last_action_result')} +Thought: {ctx.get('thought', '')} +Final Answer: {ctx.get('final_answer', '')} +""", + ) + ] + + def route_to_execute_tool(self) -> Message: context_message, route_to_stop = self._get_context_message_and_route_to_stop() - self.agent_context.update_context("Router Decision", context_message) + self.update_ctx({"router_decision": context_message}) self.iterate_and_stop_once(route_to_stop) - self.status = self.agent_context.to_data_repr() - return self.agent_context + self.status = self._create_status_data() + return Message(text=context_message, type="routing_decision") - def route_to_final_answer(self) -> AgentContext: + def route_to_final_answer(self) -> Message: context_message, route_to_stop = self._get_context_message_and_route_to_stop() - self.agent_context.update_context("Router Decision", context_message) + self.update_ctx({"router_decision": context_message}) self.iterate_and_stop_once(route_to_stop) - self.status = self.agent_context.to_data_repr() - return self.agent_context + self.status = self._create_status_data() + return Message(text=context_message, type="routing_decision") From 21612f10b0b89db8e3dfeab240ef60d1dea81c32 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 20 Dec 2024 15:13:25 -0300 Subject: [PATCH 12/15] fix: Enhance ExecuteActionComponent to improve context handling and error messaging --- .../components/agents/execute_action.py | 59 +++++++++++++------ 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/src/backend/base/langflow/components/agents/execute_action.py b/src/backend/base/langflow/components/agents/execute_action.py index b4a8dc786a0b..affc72d380f5 100644 --- a/src/backend/base/langflow/components/agents/execute_action.py +++ b/src/backend/base/langflow/components/agents/execute_action.py @@ -1,7 +1,8 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from langflow.custom import Component -from langflow.io import HandleInput, Output +from langflow.io import Output +from langflow.schema.data import Data from langflow.schema.message import Message if TYPE_CHECKING: @@ -10,27 +11,47 @@ class ExecuteActionComponent(Component): display_name = "Execute Action" - description = "Executes the decided action using available tools." + description = "Executes the selected action using available tools." - inputs = [ - HandleInput(name="agent_context", display_name="Agent Context", input_types=["AgentContext"], required=True), - ] + outputs = [Output(name="action_result", display_name="Action Result", method="execute_action")] - outputs = [Output(name="action_execution", display_name="Agent Context", method="execute_action")] + def _format_result(self, result: Any) -> str: + if hasattr(result, "content"): + return result.content + if hasattr(result, "log"): + return result.log + return str(result) def execute_action(self) -> Message: - action: AgentAction = self.agent_context.last_action + # Get the action from context + action: AgentAction = self.ctx.get("last_action") + if not action: + msg = "No action found in context to execute" + raise ValueError(msg) - tools = self.agent_context.tools + # Get tools from context + tools = self.ctx.get("tools", {}) + + # Execute the action using the appropriate tool if action.tool in tools: - data = tools[action.tool](action.tool_input) - self.agent_context.last_action_result = data - self.agent_context.update_context("Action Result", data) + result = tools[action.tool](action.tool_input) + formatted_result = self._format_result(result) + self.update_ctx({"last_action_result": formatted_result}) else: - data = f"Action '{action}' not found in available tools." - error_msg = f"Error: {data}" - self.agent_context.last_action_result = error_msg - self.agent_context.update_context("Action Result", error_msg) - tool_call_result = f"Tool: {action.tool} called with input: {action.tool_input} and returned: {data}" - self.status = self.agent_context.to_data_repr() - return Message(text=tool_call_result) + error_msg = f"Action '{action}' not found in available tools." + formatted_result = f"Error: {error_msg}" + self.update_ctx({"last_action_result": formatted_result}) + + # Create status data + self.status = [ + Data( + name="Action Execution", + value=f""" +Tool: {action.tool} +Input: {action.tool_input} +Result: {formatted_result} +""", + ) + ] + + return Message(text=formatted_result) From 573df87b29ecc9f89ad3dbab3a823856ce365f16 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 20 Dec 2024 15:13:41 -0300 Subject: [PATCH 13/15] refactor: Update DecideActionComponent to enhance context formatting and action handling --- .../components/agents/decide_action.py | 59 +++++++++++++++---- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/src/backend/base/langflow/components/agents/decide_action.py b/src/backend/base/langflow/components/agents/decide_action.py index b58b4c641509..7fbf3a3015f2 100644 --- a/src/backend/base/langflow/components/agents/decide_action.py +++ b/src/backend/base/langflow/components/agents/decide_action.py @@ -2,9 +2,10 @@ from langchain.agents.output_parsers.tools import parse_ai_message_to_tool_action -from langflow.base.agents.context import AgentContext from langflow.custom import Component -from langflow.io import HandleInput, MessageTextInput, Output +from langflow.io import MessageTextInput, Output +from langflow.schema.data import Data +from langflow.schema.message import Message if TYPE_CHECKING: from langchain_core.messages import AIMessage @@ -15,7 +16,6 @@ class DecideActionComponent(Component): description = "Decides on an action based on the current thought and context." inputs = [ - HandleInput(name="agent_context", display_name="Agent Context", input_types=["AgentContext"], required=True), MessageTextInput( name="prompt", display_name="Prompt", @@ -24,18 +24,51 @@ class DecideActionComponent(Component): ), ] - outputs = [Output(name="processed_agent_context", display_name="Agent Context", method="decide_action")] + outputs = [Output(name="action", display_name="Decided Action", method="decide_action")] - def decide_action(self) -> AgentContext: - # Append the prompt after the accumulated context following ReAct format - full_prompt = f"{self.agent_context.get_full_context()}\n{self.prompt}\nAction:" - response: AIMessage = self.agent_context.llm.invoke(full_prompt) + def _format_context(self) -> str: + ctx = self.ctx + context_parts = [] + + # Add current thought + if ctx.get("thought"): + context_parts.append(f"Current Thought: {ctx['thought']}") + + # Add available tools + if "tools" in ctx: + context_parts.append("\nAvailable Tools:") + for tool_name, tool in ctx["tools"].items(): + context_parts.append(f"- {tool_name}: {tool.description}") + + return "\n".join(context_parts) + + def decide_action(self) -> Message: + # Format the full context + full_prompt = f"{self._format_context()}\n{self.prompt}\nAction:" + + # Generate action using LLM + response: AIMessage = self.ctx["llm"].invoke(full_prompt) action = parse_ai_message_to_tool_action(response) + + # Handle action result and update context using update_ctx if isinstance(action, list): - self.agent_context.last_action = action[0] + self.update_ctx({"last_action": action[0]}) action = action[0] else: - self.agent_context.last_action = action - self.agent_context.update_context("Action", action.log) - self.status = self.agent_context.to_data_repr() - return self.agent_context + self.update_ctx({"last_action": action}) + + # Create status data + self.status = [ + Data( + name="Decided Action", + value=f""" +Context Used: +{self._format_context()} + +Decided Action: +{action.log if hasattr(action, 'log') else str(action)} +""", + ) + ] + + return Message(text=str(action)) From e81ae100762e35dbdddf3d6615a4bfd7640efbe6 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 20 Dec 2024 15:13:49 -0300 Subject: [PATCH 14/15] refactor: Remove AgentContextBuilder and enhance context initialization in LangflowAgent --- .../langflow/components/agents/new_agent.py | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/backend/base/langflow/components/agents/new_agent.py b/src/backend/base/langflow/components/agents/new_agent.py index 0907d931b1e4..a46260f2d155 100644 --- a/src/backend/base/langflow/components/agents/new_agent.py +++ b/src/backend/base/langflow/components/agents/new_agent.py @@ -1,7 +1,6 @@ from loguru import logger from langflow.components.agents.agent_action_router import AgentActionRouter -from langflow.components.agents.agent_context import AgentContextBuilder from langflow.components.agents.decide_action import DecideActionComponent from langflow.components.agents.execute_action import ExecuteActionComponent from langflow.components.agents.generate_thought import GenerateThoughtComponent @@ -51,17 +50,9 @@ async def get_response(self) -> Message: # Chat input initialization chat_input = ChatInput().set(input_value=self.user_prompt) - # Agent Context Builder - agent_context = AgentContextBuilder().set( - initial_context=chat_input.message_response, - tools=self.tools, - llm=self.llm, - max_iterations=self.max_iterations, - ) - # Generate Thought generate_thought = GenerateThoughtComponent().set( - agent_context=agent_context.build_context, + prompt="Based on the provided context, generate your next thought.", ) # Decide Action @@ -96,8 +87,23 @@ async def get_response(self) -> Message: chat_output = ChatOutput().set(input_value=final_answer.get_final_answer) agent_output_model = create_state_model("AgentOutput", output=chat_output.message_response) output_model = agent_output_model() + # Build the graph graph = Graph(chat_input, chat_output) + # Initialize the context + graph.context = { + "llm": self.llm, + "tools": self.tools, + "initial_message": chat_input.message_response, + "system_prompt": self.system_prompt, + "max_iterations": self.max_iterations, + "iteration": 0, + "thought": "", + "last_action": None, + "last_action_result": None, + "final_answer": "", + } + async for result in graph.async_start(max_iterations=self.max_iterations): if self.verbose: logger.info(result) From 197c6af2f060fce5bdfa7e217aab2bafdda4b91a Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Fri, 20 Dec 2024 15:13:54 -0300 Subject: [PATCH 15/15] feat: Enhance ProvideFinalAnswerComponent to improve context formatting and final answer generation --- .../components/agents/write_final_answer.py | 59 +++++++++++++++---- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/src/backend/base/langflow/components/agents/write_final_answer.py b/src/backend/base/langflow/components/agents/write_final_answer.py index 2c7b9d33c668..d3eaf8b73962 100644 --- a/src/backend/base/langflow/components/agents/write_final_answer.py +++ b/src/backend/base/langflow/components/agents/write_final_answer.py @@ -1,7 +1,8 @@ from typing import TYPE_CHECKING from langflow.custom import Component -from langflow.io import HandleInput, MessageTextInput, Output +from langflow.io import MessageTextInput, Output +from langflow.schema.data import Data from langflow.schema.message import Message if TYPE_CHECKING: @@ -10,10 +11,9 @@ class ProvideFinalAnswerComponent(Component): display_name = "Provide Final Answer" - description = "Generates the final answer based on the agent's context." + description = "Provides a final answer based on the context and actions taken." inputs = [ - HandleInput(name="agent_context", display_name="Agent Context", input_types=["AgentContext"], required=True), MessageTextInput( name="prompt", display_name="Prompt", @@ -22,13 +22,50 @@ class ProvideFinalAnswerComponent(Component): ), ] - outputs = [Output(name="final_answer", method="get_final_answer")] + outputs = [Output(name="final_answer", display_name="Final Answer", method="get_final_answer")] + + def _format_context(self) -> str: + ctx = self.ctx + context_parts = [] + + # Add thought if exists + if ctx.get("thought"): + context_parts.append(f"Last Thought: {ctx['thought']}") + + # Add last action and result if they exist + if ctx.get("last_action"): + context_parts.append(f"Last Action: {ctx['last_action']}") + if ctx.get("last_action_result"): + context_parts.append(f"Action Result: {ctx['last_action_result']}") + + # Add initial message for context + if ctx.get("initial_message"): + context_parts.append(f"\nInitial Query: {ctx['initial_message']}") + + return "\n".join(context_parts) def get_final_answer(self) -> Message: - # Append the prompt after the accumulated context following ReAct format - full_prompt = f"{self.agent_context.get_full_context()}\n{self.prompt}\nFinal Answer:" - final_answer: AIMessage = self.agent_context.llm.invoke(full_prompt) - self.agent_context.final_answer = final_answer - self.agent_context.update_context("Final Answer", final_answer) - self.status = self.agent_context.to_data_repr() - return Message(text=final_answer.content) + # Format the full context + full_prompt = f"{self._format_context()}\n{self.prompt}\nFinal Answer:" + + # Generate final answer using LLM + response: AIMessage = self.ctx["llm"].invoke(full_prompt) + + # Update context with final answer + self.update_ctx({"final_answer": response.content}) + + # Create status data + self.status = [ + Data( + name="Final Answer", + value=f""" +Context Used: +{self._format_context()} + +Final Answer: +{response.content} +""", + ) + ] + + return Message(text=response.content)