diff --git a/launch/launch/agent/base_image.py b/launch/launch/agent/base_image.py index 994c0fe..30285d8 100644 --- a/launch/launch/agent/base_image.py +++ b/launch/launch/agent/base_image.py @@ -1,9 +1,13 @@ """ Base Docker image selection agent for repository environment setup. """ -from langchain.schema import HumanMessage +try: # LangChain >= 0.3.26 + from langchain_core.messages import HumanMessage # type: ignore[import-not-found] +except ImportError: # pragma: no cover - fallback for older LangChain + from langchain.schema import HumanMessage # type: ignore from launch.agent.state import AgentState, auto_catch +from launch.agent.utils import message_content_to_str from launch.utilities.language_handlers import get_language_handler @@ -49,8 +53,9 @@ def select_base_image(state: AgentState) -> dict: while not base_image or trials < 5: trials += 1 response = llm.invoke(messages) - if "" in response.content: - image = response.content.split("")[1].split("")[0] + response_text = message_content_to_str(response.content) + if "" in response_text: + image = response_text.split("")[1].split("")[0] if image in candidate_images: base_image = image break diff --git a/launch/launch/agent/locate.py b/launch/launch/agent/locate.py index a896054..b0e5e31 100644 --- a/launch/launch/agent/locate.py +++ b/launch/launch/agent/locate.py @@ -3,10 +3,14 @@ """ import os -from langchain.schema import HumanMessage +try: # LangChain >= 0.3.26 + from langchain_core.messages import HumanMessage # type: ignore[import-not-found] +except ImportError: # pragma: no cover + from langchain.schema import HumanMessage # type: ignore from launch.agent.state import AgentState, auto_catch from launch.utilities.get_repo_structure import view_repo_structure +from launch.agent.utils import message_content_to_str prompt = """Given this repository structure: ------ BEGIN REPOSITORY STRUCTURE ------ @@ -67,9 +71,10 @@ def locate_related_file(state: AgentState) -> dict: ) response = llm.invoke([locate_prompt]) + response_text = message_content_to_str(response.content) potential_files = [ line.split("")[1].split("")[0].strip() - for line in response.content.split("\n") + for line in response_text.split("\n") if line.strip() and "" in line ] potential_files = [ @@ -103,11 +108,12 @@ def locate_related_file(state: AgentState) -> dict: determine_input = HumanMessage(content=determine_prompt.format(file=file_info)) try: response = llm.invoke([determine_input]) + response_text = message_content_to_str(response.content) except Exception: logger.error(f"Error determining file: {file}") continue - logger.info(f"File: {file} - {response.content}") - if "Yes" in response.content: + logger.info(f"File: {file} - {response_text}") + if "Yes" in response_text: docs += f"File: {file}\n```\n" docs += content + "\n" docs += "```\n" diff --git a/launch/launch/agent/setup.py b/launch/launch/agent/setup.py index 1bf26f4..a8065d0 100644 --- a/launch/launch/agent/setup.py +++ b/launch/launch/agent/setup.py @@ -6,7 +6,13 @@ import time from typing import Any, Literal -from langchain_core.messages import HumanMessage, SystemMessage +try: # LangChain >= 0.3.26 + from langchain_core.messages import ( # type: ignore[import-not-found] + HumanMessage, + SystemMessage, + ) +except ImportError: # pragma: no cover + from langchain.schema import HumanMessage, SystemMessage # type: ignore from pydantic import BaseModel, Field from launch.agent.action_parser import ActionParser @@ -14,6 +20,7 @@ from launch.agent.state import AgentState, auto_catch from launch.runtime import start_session from launch.utilities.language_handlers import get_language_handler +from launch.agent.utils import message_content_to_str system_msg = """You are a developer. Your task is to install dependencies and set up a environment that is able to run the tests of the project. @@ -224,11 +231,10 @@ def setup(max_steps: int, state: AgentState) -> dict: response = llm.invoke(input_messages) - # print(response.pretty_repr()) logger.info("\n" + response.pretty_repr()) messages.append(response) - action = parse_setup_action(response.content) + action = parse_setup_action(message_content_to_str(response.content)) if action and action.action == "command": commands.append(action.args) observation = observation_for_setup_action(state, action) diff --git a/launch/launch/agent/utils.py b/launch/launch/agent/utils.py new file mode 100644 index 0000000..5f31004 --- /dev/null +++ b/launch/launch/agent/utils.py @@ -0,0 +1,44 @@ +""" +Utility helpers shared across agent modules. +""" +from __future__ import annotations + +from typing import Any + + +def message_content_to_str(content: Any) -> str: + """ + Normalize LangChain message content into a plain string. + + Newer LangChain versions return message.content as either a raw string + or a list of structured blocks (e.g., [{"type": "text", "text": "..."}]). + This helper flattens those structures so legacy string-based parsing + logic keeps working. + """ + if content is None: + return "" + + if isinstance(content, str): + return content + + if isinstance(content, list): + parts: list[str] = [] + for block in content: + if isinstance(block, str): + parts.append(block) + elif isinstance(block, dict): + if block.get("type") == "text": + parts.append(block.get("text", "")) + elif "text" in block: + parts.append(str(block["text"])) + elif "content" in block: + parts.append(str(block["content"])) + else: + # Fallback to the full dict representation to preserve info + parts.append(str(block)) + else: + parts.append(str(block)) + return "".join(parts) + + return str(content) + diff --git a/launch/launch/agent/verify.py b/launch/launch/agent/verify.py index d5227f9..e44d917 100644 --- a/launch/launch/agent/verify.py +++ b/launch/launch/agent/verify.py @@ -3,13 +3,20 @@ """ from typing import Any, Literal -from langchain.schema import HumanMessage, SystemMessage +try: # LangChain >= 0.3.26 + from langchain_core.messages import ( # type: ignore[import-not-found] + HumanMessage, + SystemMessage, + ) +except ImportError: # pragma: no cover + from langchain.schema import HumanMessage, SystemMessage # type: ignore from pydantic import BaseModel, Field from launch.agent.action_parser import ActionParser from launch.agent.prompt import ReAct_prompt from launch.agent.state import AgentState, auto_catch from launch.runtime import SetupRuntime +from launch.agent.utils import message_content_to_str system_msg = """You are a developer. Your task is to verify whether the environment for the given project is set up correctly. Your colleague has set up a Docker environment for the project. You need to verify if it can successfully run the tests of the project. - You interact with a Bash session inside this container. @@ -170,7 +177,7 @@ def verify(max_steps: int, state: AgentState) -> dict: # print(response.pretty_repr()) logger.info(response.pretty_repr()) messages.append(response) - action = parse_verify_action(response.content) + action = parse_verify_action(message_content_to_str(response.content)) if action.action == "command": commands.append(action.args) observation = observation_for_verify_action(action, session)