From 5efff05e92598debe68529d0cff106adab3b0da0 Mon Sep 17 00:00:00 2001 From: Noravee Kanchanavatee Date: Mon, 28 Jul 2025 10:08:32 -0700 Subject: [PATCH 1/9] create originating journal for tools and write message in on_tool_end() --- .../langchain/core/langchain_run_context.py | 30 +++++++------- .../journaling/journaling_callback_handler.py | 39 ++++++++++++++++--- 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/neuro_san/internals/run_context/langchain/core/langchain_run_context.py b/neuro_san/internals/run_context/langchain/core/langchain_run_context.py index 324167f21..6e04c7416 100644 --- a/neuro_san/internals/run_context/langchain/core/langchain_run_context.py +++ b/neuro_san/internals/run_context/langchain/core/langchain_run_context.py @@ -168,19 +168,8 @@ async def create_resources(self, agent_name: str, # DEF - Remove the arg if possible _ = agent_name - # Create the list of callbacks to pass to the LLM ChatModel - callbacks: List[BaseCallbackHandler] = [ - JournalingCallbackHandler(self.journal) - ] full_name: str = Origination.get_full_name_from_origin(self.origin) - - # Consult the agent spec for level of verbosity as it pertains to callbacks. agent_spec: Dict[str, Any] = self.tool_caller.get_agent_tool_spec() - verbose: Union[bool, str] = agent_spec.get("verbose", False) - if isinstance(verbose, str) and verbose.lower() in ("extra", "logging"): - # This particular class adds a *lot* of very detailed messages - # to the logs. Add this because some people are interested in it. - callbacks.append(LoggingCallbackHandler(self.logger)) # Now that we have a name, we can create an ErrorDetector for the output. self.error_detector = ErrorDetector(full_name, @@ -199,10 +188,10 @@ async def create_resources(self, agent_name: str, prompt_template: ChatPromptTemplate = await self._create_prompt_template(instructions) - self.agent = self.create_agent_with_fallbacks(prompt_template, callbacks) + self.agent = self.create_agent_with_fallbacks(prompt_template) def create_agent_with_fallbacks(self, prompt_template: ChatPromptTemplate, - callbacks: List[BaseCallbackHandler]) -> Agent: + callbacks: List[BaseCallbackHandler] = None) -> Agent: """ Creates an agent with potential fallback llms to use. :param prompt_template: The ChatPromptTemplate to use for the agent @@ -451,10 +440,23 @@ async def wait_on_run(self, run: Run, journal: Journal = None) -> Run: "chat_history": previous_chat_history, "input": self.recent_human_message } + + # Create the list of callbacks to pass to the LLM ChatModel + callbacks: List[BaseCallbackHandler] = [ + JournalingCallbackHandler(self, self.journal) + ] + # Consult the agent spec for level of verbosity as it pertains to callbacks. + agent_spec: Dict[str, Any] = self.tool_caller.get_agent_tool_spec() + verbose: Union[bool, str] = agent_spec.get("verbose", False) + if isinstance(verbose, str) and verbose.lower() in ("extra", "logging"): + # This particular class adds a *lot* of very detailed messages + # to the logs. Add this because some people are interested in it. + callbacks.append(LoggingCallbackHandler(self.logger)) invoke_config = { "configurable": { "session_id": run.get_id() - } + }, + "callbacks": callbacks } # Chat history is updated in write_message diff --git a/neuro_san/internals/run_context/langchain/journaling/journaling_callback_handler.py b/neuro_san/internals/run_context/langchain/journaling/journaling_callback_handler.py index 2c2a9807f..756236be9 100644 --- a/neuro_san/internals/run_context/langchain/journaling/journaling_callback_handler.py +++ b/neuro_san/internals/run_context/langchain/journaling/journaling_callback_handler.py @@ -22,8 +22,11 @@ from langchain_core.outputs import LLMResult from langchain_core.outputs.chat_generation import ChatGeneration +from neuro_san.internals.journals.journal import Journal from neuro_san.internals.journals.originating_journal import OriginatingJournal +from neuro_san.internals.messages.origination import Origination from neuro_san.internals.messages.agent_message import AgentMessage +from neuro_san.internals.run_context.interfaces.run_context import RunContext # pylint: disable=too-many-ancestors @@ -52,12 +55,13 @@ class JournalingCallbackHandler(AsyncCallbackHandler): # a non-pydantic Journal as a member, we need to do this. model_config = ConfigDict(arbitrary_types_allowed=True) - def __init__(self, journal: OriginatingJournal): + def __init__(self, run_context: RunContext, journal: OriginatingJournal): """ Constructor :param journal: The journal to write messages to """ + self.run_context: RunContext = run_context self.journal: OriginatingJournal = journal async def on_llm_end(self, response: LLMResult, @@ -66,11 +70,13 @@ async def on_llm_end(self, response: LLMResult, # have a generations field which is a list of lists. Inside that inner list, # the first object is a ChatGeneration, whose text field tends to have agent # thinking in it. + print(f"\n\n{response=}\n\n") generations = response.generations[0] first_generation = generations[0] if isinstance(first_generation, ChatGeneration): content: str = first_generation.text if content is not None and len(content) > 0: + print(f"\n\n{content=}\n\n") # Package up the thinking content as an AgentMessage to stream message = AgentMessage(content=content.strip()) # Some AGENT messages that come from this source end up being dupes @@ -83,19 +89,42 @@ async def on_chain_end(self, outputs: Dict[str, Any], # print(f"In on_chain_end() with {outputs}") return + async def on_tool_start(self, serialized, input_str, *, run_id, parent_run_id = None, tags = None, metadata = None, inputs = None, **kwargs): + print(f"\n\n{serialized=}\n\n") + print(f"\n\n{input_str=}\n\n") + print(f"\n\n{inputs=}\n\n") + print(f"\n\n{kwargs=}\n\n") + async def on_tool_end(self, output: Any, **kwargs: Any) -> None: - # print(f"In on_tool_end() with {output}") - return + print(f"\n\nIn on_tool_end() with {output}\n\n") + print(f"\n\noutput_{kwargs=}\n\n") + print(f"\n\n{kwargs.get("name")=}\n\n") + if isinstance(output, str): + agent_name = kwargs.get("name") + # 1. Get the base journal from invocation context + base_journal: Journal = self.run_context.invocation_context.get_journal() + + # 2. Build the origin path + parent_origin = self.run_context.get_origin() + print(f"\n\njournal_{parent_origin=}\n\n") + # agent_name: str = self.run_context.tool_caller.get_name() + origination: Origination = self.run_context.invocation_context.get_origination() + origin = origination.add_spec_name_to_origin(parent_origin, agent_name) + print(f"\n\njournal_{origin=}\n\n") + + # # 3. Create the OriginatingJournal + journal = OriginatingJournal(base_journal, origin) + await journal.write_message(AgentMessage(output)) async def on_agent_action(self, action: AgentAction, **kwargs: Any) -> None: - # print(f"In on_agent_action() with {action}") + print(f"In on_agent_action() with {action}") return async def on_agent_finish(self, finish: AgentFinish, **kwargs: Any) -> None: - # print(f"In on_agent_finish() with {finish}") + print(f"In on_agent_finish() with {finish}") return async def on_retriever_end(self, documents: Sequence[Document], From 3864dde8e4af4449bbc192eb3f64b4f38a827fa2 Mon Sep 17 00:00:00 2001 From: Noravee Kanchanavatee Date: Tue, 29 Jul 2025 02:15:14 -0700 Subject: [PATCH 2/9] Add tags for langchain tool and use it to identify langchain tool --- .../langchain/core/langchain_run_context.py | 11 +- .../journaling/journaling_callback_handler.py | 122 ++++++++++++------ .../langchain/toolbox/toolbox_factory.py | 19 ++- 3 files changed, 109 insertions(+), 43 deletions(-) diff --git a/neuro_san/internals/run_context/langchain/core/langchain_run_context.py b/neuro_san/internals/run_context/langchain/core/langchain_run_context.py index 6e04c7416..347bc7f00 100644 --- a/neuro_san/internals/run_context/langchain/core/langchain_run_context.py +++ b/neuro_san/internals/run_context/langchain/core/langchain_run_context.py @@ -313,7 +313,7 @@ async def _create_base_tool(self, name: str) -> BaseTool: if toolbox: toolbox_factory: ContextTypeToolboxFactory = self.invocation_context.get_toolbox_factory() try: - tool_from_toolbox = toolbox_factory.create_tool_from_toolbox(toolbox, agent_spec.get("args")) + tool_from_toolbox = toolbox_factory.create_tool_from_toolbox(toolbox, agent_spec.get("args"), name) # If the tool from toolbox is base tool or list of base tool, return the tool as is # since tool's definition and args schema are predefined in these the class of the tool. if isinstance(tool_from_toolbox, BaseTool) or ( @@ -441,9 +441,12 @@ async def wait_on_run(self, run: Run, journal: Journal = None) -> Run: "input": self.recent_human_message } - # Create the list of callbacks to pass to the LLM ChatModel + # Create the list of callbacks to pass when invoking + parent_origin: List[Dict[str, Any]] = self.get_origin() + base_journal: Journal = self.invocation_context.get_journal() + origination: Origination = self.invocation_context.get_origination() callbacks: List[BaseCallbackHandler] = [ - JournalingCallbackHandler(self, self.journal) + JournalingCallbackHandler(base_journal, self.journal, parent_origin, origination) ] # Consult the agent spec for level of verbosity as it pertains to callbacks. agent_spec: Dict[str, Any] = self.tool_caller.get_agent_tool_spec() @@ -452,6 +455,8 @@ async def wait_on_run(self, run: Run, journal: Journal = None) -> Run: # This particular class adds a *lot* of very detailed messages # to the logs. Add this because some people are interested in it. callbacks.append(LoggingCallbackHandler(self.logger)) + + # Add callbacks as an invoke config invoke_config = { "configurable": { "session_id": run.get_id() diff --git a/neuro_san/internals/run_context/langchain/journaling/journaling_callback_handler.py b/neuro_san/internals/run_context/langchain/journaling/journaling_callback_handler.py index 756236be9..c89946531 100644 --- a/neuro_san/internals/run_context/langchain/journaling/journaling_callback_handler.py +++ b/neuro_san/internals/run_context/langchain/journaling/journaling_callback_handler.py @@ -12,6 +12,7 @@ from collections.abc import Sequence from typing import Any from typing import Dict +from typing import List from pydantic import ConfigDict @@ -19,6 +20,8 @@ from langchain_core.agents import AgentFinish from langchain_core.callbacks.base import AsyncCallbackHandler from langchain_core.documents import Document +from langchain_core.messages.ai import AIMessage +from langchain_core.messages.base import BaseMessage from langchain_core.outputs import LLMResult from langchain_core.outputs.chat_generation import ChatGeneration @@ -26,7 +29,6 @@ from neuro_san.internals.journals.originating_journal import OriginatingJournal from neuro_san.internals.messages.origination import Origination from neuro_san.internals.messages.agent_message import AgentMessage -from neuro_san.internals.run_context.interfaces.run_context import RunContext # pylint: disable=too-many-ancestors @@ -55,14 +57,29 @@ class JournalingCallbackHandler(AsyncCallbackHandler): # a non-pydantic Journal as a member, we need to do this. model_config = ConfigDict(arbitrary_types_allowed=True) - def __init__(self, run_context: RunContext, journal: OriginatingJournal): + def __init__( + self, + base_journal: Journal, + calling_agent_journal: Journal, + parent_origin: List[Dict[str, Any]], + origination: Origination + ): """ Constructor - :param journal: The journal to write messages to + :param base_journal: The Journal instance that allows message reporting during the course of the AgentSession. + This is used to construct the langchain_tool_journal. + :param calling_agent_journal: The journal of the calling agent + :param parent_origin: A List of origin dictionaries indicating the origin of the run + This is used to construct the langchain_tool_journal. + :param origination: The Origination instance carrying state about tool instantation + during the course of the AgentSession. This is used to construct the langchain_tool_journal. """ - self.run_context: RunContext = run_context - self.journal: OriginatingJournal = journal + self.base_journal: Journal = base_journal + self.calling_agent_journal: Journal = calling_agent_journal + self.parent_origin: List[Dict[str, Any]] = parent_origin + self.origination: Origination = origination + self.langchain_tool_journal: Journal = None async def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None: @@ -70,61 +87,92 @@ async def on_llm_end(self, response: LLMResult, # have a generations field which is a list of lists. Inside that inner list, # the first object is a ChatGeneration, whose text field tends to have agent # thinking in it. - print(f"\n\n{response=}\n\n") generations = response.generations[0] first_generation = generations[0] if isinstance(first_generation, ChatGeneration): content: str = first_generation.text if content is not None and len(content) > 0: - print(f"\n\n{content=}\n\n") # Package up the thinking content as an AgentMessage to stream message = AgentMessage(content=content.strip()) # Some AGENT messages that come from this source end up being dupes # of AI messages that can come later. # Use this method to put the message on hold for later comparison. - await self.journal.write_message_if_next_not_dupe(message) + await self.calling_agent_journal.write_message_if_next_not_dupe(message) async def on_chain_end(self, outputs: Dict[str, Any], **kwargs: Any) -> None: # print(f"In on_chain_end() with {outputs}") return - async def on_tool_start(self, serialized, input_str, *, run_id, parent_run_id = None, tags = None, metadata = None, inputs = None, **kwargs): - print(f"\n\n{serialized=}\n\n") - print(f"\n\n{input_str=}\n\n") - print(f"\n\n{inputs=}\n\n") - print(f"\n\n{kwargs=}\n\n") - - async def on_tool_end(self, output: Any, - **kwargs: Any) -> None: - print(f"\n\nIn on_tool_end() with {output}\n\n") - print(f"\n\noutput_{kwargs=}\n\n") - print(f"\n\n{kwargs.get("name")=}\n\n") - if isinstance(output, str): - agent_name = kwargs.get("name") - # 1. Get the base journal from invocation context - base_journal: Journal = self.run_context.invocation_context.get_journal() - - # 2. Build the origin path - parent_origin = self.run_context.get_origin() - print(f"\n\njournal_{parent_origin=}\n\n") - # agent_name: str = self.run_context.tool_caller.get_name() - origination: Origination = self.run_context.invocation_context.get_origination() - origin = origination.add_spec_name_to_origin(parent_origin, agent_name) - print(f"\n\njournal_{origin=}\n\n") - - # # 3. Create the OriginatingJournal - journal = OriginatingJournal(base_journal, origin) - await journal.write_message(AgentMessage(output)) + async def on_tool_start( + self, + serialized: Dict[str, Any], + input_str: str, + tags: List[str] = None, + inputs: Dict[str, Any] = None, + **kwargs: Any + ) -> None: + """ + Callback triggered when a tool starts execution. + + If the tool is identified as a LangChain tool (via the "langchain_tool" tag), + this method creates a journal entry containing the tool's input arguments, + origin metadata, and full tool name. + + :param serialized: Serialized representation of the tool, including its name and description. + :param input_str: String representation of the tool's input. + :param tags: List of tags associated with the tool. Used to determine whether it is a LangChain tool. + :param inputs: Structured dictionary of input arguments + passed to the tool. + """ + + if "langchain_tool" in tags: + + # Extract tool name from the serialized data + agent_name: str = serialized.get("name") + + # Build the origin path + origin: List[Dict[str, Any]] = self.origination.add_spec_name_to_origin(self.parent_origin, agent_name) + full_name: str = self.origination.get_full_name_from_origin(origin) + + # Combine the original tool inputs with origin metadata + combined_args: Dict[str, Any] = inputs.copy() + combined_args["origin"] = origin + combined_args["origin_str"] = full_name + + # Create a journal entry for this invocation and log the combined inputs + self.langchain_tool_journal = OriginatingJournal(self.base_journal, origin) + message: BaseMessage = AgentMessage(content=f"Received arguments {combined_args}") + await self.langchain_tool_journal.write_message(message) + + async def on_tool_end(self, output: Any, tags: List[str] = None, **kwargs: Any) -> None: + """ + Callback triggered when a tool finishes execution. + + If the tool is identified as a LangChain tool (via the "langchain_tool" tag), + this method logs the tool's output to both the calling agent's journal and the + LangChain tool's specific journal. + + :param output: The result produced by the tool after execution. + :param tags: List of tags associated with the tool. Used to determine whether it is a LangChain tool. + """ + + if "langchain_tool" in tags: + # Log the tool output to the calling agent's journal + await self.calling_agent_journal.write_message(AIMessage(content=output)) + + # Also log the tool output to the LangChain tool-specific journal + message: BaseMessage = AgentMessage(content=f"Got result: {output}") + await self.langchain_tool_journal.write_message(message) async def on_agent_action(self, action: AgentAction, **kwargs: Any) -> None: - print(f"In on_agent_action() with {action}") + # print(f"In on_agent_action() with {action}") return async def on_agent_finish(self, finish: AgentFinish, **kwargs: Any) -> None: - print(f"In on_agent_finish() with {finish}") + # print(f"In on_agent_finish() with {finish}") return async def on_retriever_end(self, documents: Sequence[Document], diff --git a/neuro_san/internals/run_context/langchain/toolbox/toolbox_factory.py b/neuro_san/internals/run_context/langchain/toolbox/toolbox_factory.py index 1e6db816d..36e06cfee 100644 --- a/neuro_san/internals/run_context/langchain/toolbox/toolbox_factory.py +++ b/neuro_san/internals/run_context/langchain/toolbox/toolbox_factory.py @@ -116,13 +116,16 @@ def load(self): def create_tool_from_toolbox( self, tool_name: str, - user_args: Dict[str, Any] = None + user_args: Dict[str, Any] = None, + agent_name: str = None ) -> Union[BaseTool, Dict[str, Any], List[BaseTool]]: """ Resolves dependencies and instantiates the requested tool. :param tool_name: The name of the tool to instantiate. :param user_args: Arguments provided by the user, which override the config file. + :param agent_name: The name of the agent to prefix each BaseTool's name in BaseToolkit with, + ensuring tool names are unique and traceable to their agent. :return: - Instantiated tool if "class" of tool_name points to a BaseTool class - A list of tools if "class of "tool_name points to a BaseToolkit class. - A dict of tool's "description" and "parameters" if tool_name points to a CodedTool @@ -168,8 +171,18 @@ def create_tool_from_toolbox( # If the instantiated class has "get_tools()", assume it's a toolkit and return a list of tools if hasattr(instance, "get_tools") and callable(instance.get_tools): - return instance.get_tools() - + toolkit: List[BaseTool] = instance.get_tools() + for tool in toolkit: + # Prefix the name of the agent to each tool + tool.name = f"{agent_name}_{tool.name}" + # Add "langchain_tool" tags so journal callback can idenitify it + tool.tags = ["langchain_tool"] + return toolkit + + # Replace langchain tool's name with agent name + instance.name = agent_name + # Add "langchain_tool" tags so journal callback can idenitify it + instance.tags = ["langchain_tool"] return instance def _resolve_args(self, args: Dict[str, Any]) -> Dict[str, Any]: From 9a9e912259705081b9f2b6689d18d880432e6528 Mon Sep 17 00:00:00 2001 From: Noravee Kanchanavatee Date: Tue, 29 Jul 2025 02:44:18 -0700 Subject: [PATCH 3/9] Add attr "name" and "tags" in test_toolbox_factory --- .../langchain/toolbox/test_toolbox_factory.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/neuro_san/internals/run_context/langchain/toolbox/test_toolbox_factory.py b/tests/neuro_san/internals/run_context/langchain/toolbox/test_toolbox_factory.py index 0579d450c..a922d8825 100644 --- a/tests/neuro_san/internals/run_context/langchain/toolbox/test_toolbox_factory.py +++ b/tests/neuro_san/internals/run_context/langchain/toolbox/test_toolbox_factory.py @@ -54,6 +54,8 @@ def test_create_toolbox_returns_single_base_tool(self, factory): mock_resolver.return_value = mock_tool_class mock_instance = MagicMock(spec=BaseTool) + mock_instance.name = MagicMock(spec=str) + mock_instance.tags = MagicMock(spec=list) mock_tool_class.return_value = mock_instance tool = factory.create_tool_from_toolbox("test_tool", user_args) @@ -90,7 +92,13 @@ def test_create_toolbox_with_toolkit_constructor(self, factory): mock_resolver.return_value = mock_toolkit_class mock_instance = MagicMock() - mock_tools = [MagicMock(spec=BaseTool), MagicMock(spec=BaseTool)] + mock_tool_1 = MagicMock(spec=BaseTool) + mock_tool_1.name = MagicMock(spec=str) + mock_tool_1.tags = MagicMock(spec=list) + mock_tool_2 = MagicMock(spec=BaseTool) + mock_tool_2.name = MagicMock(spec=str) + mock_tool_2.tags = MagicMock(spec=list) + mock_tools = [mock_tool_1, mock_tool_2] mock_instance.get_tools.return_value = mock_tools mock_toolkit_class.return_value = mock_instance @@ -134,7 +142,11 @@ def test_create_toolbox_with_toolkit_class_method(self, factory): # Mock get_tools() returning a list of tools mock_tool_1 = MagicMock(spec=BaseTool) + mock_tool_1.name = MagicMock(spec=str) + mock_tool_1.tags = MagicMock(spec=list) mock_tool_2 = MagicMock(spec=BaseTool) + mock_tool_2.name = MagicMock(spec=str) + mock_tool_2.tags = MagicMock(spec=list) mock_toolkit_instance.get_tools.return_value = [mock_tool_1, mock_tool_2] # Call the factory method From 5245ccc44057d20d1e370e0fb05329feffc41273 Mon Sep 17 00:00:00 2001 From: Noravee Kanchanavatee Date: Tue, 29 Jul 2025 03:12:10 -0700 Subject: [PATCH 4/9] switch position between base_journal and calling_agent_journal --- .../langchain/journaling/journaling_callback_handler.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/neuro_san/internals/run_context/langchain/journaling/journaling_callback_handler.py b/neuro_san/internals/run_context/langchain/journaling/journaling_callback_handler.py index c89946531..5ca9960fe 100644 --- a/neuro_san/internals/run_context/langchain/journaling/journaling_callback_handler.py +++ b/neuro_san/internals/run_context/langchain/journaling/journaling_callback_handler.py @@ -59,24 +59,24 @@ class JournalingCallbackHandler(AsyncCallbackHandler): def __init__( self, - base_journal: Journal, calling_agent_journal: Journal, + base_journal: Journal, parent_origin: List[Dict[str, Any]], origination: Origination ): """ Constructor + :param calling_agent_journal: The journal of the calling agent :param base_journal: The Journal instance that allows message reporting during the course of the AgentSession. This is used to construct the langchain_tool_journal. - :param calling_agent_journal: The journal of the calling agent :param parent_origin: A List of origin dictionaries indicating the origin of the run This is used to construct the langchain_tool_journal. :param origination: The Origination instance carrying state about tool instantation during the course of the AgentSession. This is used to construct the langchain_tool_journal. """ - self.base_journal: Journal = base_journal self.calling_agent_journal: Journal = calling_agent_journal + self.base_journal: Journal = base_journal self.parent_origin: List[Dict[str, Any]] = parent_origin self.origination: Origination = origination self.langchain_tool_journal: Journal = None From fa1dd364d1b59547176b39e9a44eb04e9dc32967 Mon Sep 17 00:00:00 2001 From: Noravee Kanchanavatee Date: Tue, 29 Jul 2025 03:34:25 -0700 Subject: [PATCH 5/9] switch args in langchain run context --- .../run_context/langchain/core/langchain_run_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neuro_san/internals/run_context/langchain/core/langchain_run_context.py b/neuro_san/internals/run_context/langchain/core/langchain_run_context.py index 347bc7f00..62655924d 100644 --- a/neuro_san/internals/run_context/langchain/core/langchain_run_context.py +++ b/neuro_san/internals/run_context/langchain/core/langchain_run_context.py @@ -446,7 +446,7 @@ async def wait_on_run(self, run: Run, journal: Journal = None) -> Run: base_journal: Journal = self.invocation_context.get_journal() origination: Origination = self.invocation_context.get_origination() callbacks: List[BaseCallbackHandler] = [ - JournalingCallbackHandler(base_journal, self.journal, parent_origin, origination) + JournalingCallbackHandler(self.journal, base_journal, parent_origin, origination) ] # Consult the agent spec for level of verbosity as it pertains to callbacks. agent_spec: Dict[str, Any] = self.tool_caller.get_agent_tool_spec() From 742e16ddfdf54da9cc84f884e700a8fb3b1ba86b Mon Sep 17 00:00:00 2001 From: Noravee Kanchanavatee Date: Tue, 29 Jul 2025 03:52:11 -0700 Subject: [PATCH 6/9] change class name of test_toolbox_factory to TestToolboxFactory --- .../run_context/langchain/toolbox/test_toolbox_factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/neuro_san/internals/run_context/langchain/toolbox/test_toolbox_factory.py b/tests/neuro_san/internals/run_context/langchain/toolbox/test_toolbox_factory.py index a922d8825..2c38ab91c 100644 --- a/tests/neuro_san/internals/run_context/langchain/toolbox/test_toolbox_factory.py +++ b/tests/neuro_san/internals/run_context/langchain/toolbox/test_toolbox_factory.py @@ -25,7 +25,7 @@ ) -class TestBaseToolFactory: +class TestToolboxFactory: """Simplified test suite for ToolboxFactory.""" @pytest.fixture From 29d5afd02d6db87b3fd8e6c93798ddadb91da620 Mon Sep 17 00:00:00 2001 From: Noravee Kanchanavatee Date: Tue, 29 Jul 2025 11:18:06 -0700 Subject: [PATCH 7/9] remove callbacks args from LangChainRunContext.create_agent_with_fallbacks() --- .../run_context/langchain/core/langchain_run_context.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/neuro_san/internals/run_context/langchain/core/langchain_run_context.py b/neuro_san/internals/run_context/langchain/core/langchain_run_context.py index 62655924d..89c4f0e17 100644 --- a/neuro_san/internals/run_context/langchain/core/langchain_run_context.py +++ b/neuro_san/internals/run_context/langchain/core/langchain_run_context.py @@ -190,12 +190,10 @@ async def create_resources(self, agent_name: str, self.agent = self.create_agent_with_fallbacks(prompt_template) - def create_agent_with_fallbacks(self, prompt_template: ChatPromptTemplate, - callbacks: List[BaseCallbackHandler] = None) -> Agent: + def create_agent_with_fallbacks(self, prompt_template: ChatPromptTemplate) -> Agent: """ Creates an agent with potential fallback llms to use. :param prompt_template: The ChatPromptTemplate to use for the agent - :param callbacks: The list of callbacks to use when creating any LLM via the factory :return: An Agent (Runnable) """ # Initialize our return value @@ -215,7 +213,7 @@ def create_agent_with_fallbacks(self, prompt_template: ChatPromptTemplate, for index, fallback in enumerate(fallbacks): # Create a model we might use. - one_llm: BaseLanguageModel = llm_factory.create_llm(fallback, callbacks=callbacks) + one_llm: BaseLanguageModel = llm_factory.create_llm(fallback) one_agent: Agent = self.create_agent(prompt_template, one_llm) if index == 0: From 5ed68794aa754aa9178e95d7c8b497f5ef3ad52a Mon Sep 17 00:00:00 2001 From: Noravee Kanchanavatee Date: Wed, 30 Jul 2025 14:35:50 -0700 Subject: [PATCH 8/9] - Add comment about langchain_tool_journal - Minor modification in toolbox_factory.create_tool_from_toolbox() --- .../journaling/journaling_callback_handler.py | 10 ++++++++++ .../run_context/langchain/toolbox/toolbox_factory.py | 10 ++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/neuro_san/internals/run_context/langchain/journaling/journaling_callback_handler.py b/neuro_san/internals/run_context/langchain/journaling/journaling_callback_handler.py index 5ca9960fe..d910311c0 100644 --- a/neuro_san/internals/run_context/langchain/journaling/journaling_callback_handler.py +++ b/neuro_san/internals/run_context/langchain/journaling/journaling_callback_handler.py @@ -75,6 +75,16 @@ def __init__( :param origination: The Origination instance carrying state about tool instantation during the course of the AgentSession. This is used to construct the langchain_tool_journal. """ + + # The calling-agent journal logs the execution flow from the perspective of the agent invoking the tool + # (e.g., MusicNerdPro). In contrast, the LangChain tool journal represents the tool's own execution + # context—similar to how coded tools like Accountant have their own journal tied to their run context. + + # LangChain tools don’t instantiate their own RunContext, so they lack a dedicated journal by default. + # To maintain consistency with how other tools are tracked, we explicitly create a langchain_tool_journal + # when the tool starts. This ensures tool-specific inputs and outputs are captured independently, + # while still allowing the calling agent to log its own perspective. + self.calling_agent_journal: Journal = calling_agent_journal self.base_journal: Journal = base_journal self.parent_origin: List[Dict[str, Any]] = parent_origin diff --git a/neuro_san/internals/run_context/langchain/toolbox/toolbox_factory.py b/neuro_san/internals/run_context/langchain/toolbox/toolbox_factory.py index 36e06cfee..bb01467ef 100644 --- a/neuro_san/internals/run_context/langchain/toolbox/toolbox_factory.py +++ b/neuro_san/internals/run_context/langchain/toolbox/toolbox_factory.py @@ -173,14 +173,16 @@ def create_tool_from_toolbox( if hasattr(instance, "get_tools") and callable(instance.get_tools): toolkit: List[BaseTool] = instance.get_tools() for tool in toolkit: - # Prefix the name of the agent to each tool - tool.name = f"{agent_name}_{tool.name}" + if agent_name: + # Prefix the name of the agent to each tool + tool.name = f"{agent_name}_{tool.name}" # Add "langchain_tool" tags so journal callback can idenitify it tool.tags = ["langchain_tool"] return toolkit - # Replace langchain tool's name with agent name - instance.name = agent_name + if agent_name: + # Replace langchain tool's name with agent name + instance.name = agent_name # Add "langchain_tool" tags so journal callback can idenitify it instance.tags = ["langchain_tool"] return instance From 7985e062f023df1a11474f682c231b2166baa898 Mon Sep 17 00:00:00 2001 From: Noravee Kanchanavatee Date: Wed, 30 Jul 2025 14:53:49 -0700 Subject: [PATCH 9/9] Add comment in toolbox_factory.create_tool_from_toolbox() --- .../run_context/langchain/toolbox/toolbox_factory.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/neuro_san/internals/run_context/langchain/toolbox/toolbox_factory.py b/neuro_san/internals/run_context/langchain/toolbox/toolbox_factory.py index bb01467ef..fa39216e2 100644 --- a/neuro_san/internals/run_context/langchain/toolbox/toolbox_factory.py +++ b/neuro_san/internals/run_context/langchain/toolbox/toolbox_factory.py @@ -130,6 +130,11 @@ def create_tool_from_toolbox( - A list of tools if "class of "tool_name points to a BaseToolkit class. - A dict of tool's "description" and "parameters" if tool_name points to a CodedTool """ + + # agent_name is required when the tool is used as an internal agent. + # However, tools from the toolbox could potentially be used as external tools, + # in which case agent_name may not be needed. + empty: Dict[str, Any] = {} tool_info: Dict[str, Any] = self.toolbox_infos.get(tool_name)