An agent migration can keep a plausible answer while the tool behind it receives different arguments or never runs. Moving legacy LlamaIndex agents to FunctionAgent therefore needs a comparison of the tool arguments and final answer before the old dependency is removed.
LlamaIndex removed its older agent classes in core 0.13, including AgentRunner, FunctionCallingAgent, and OpenAIAgent. FunctionAgent uses asynchronous workflow execution instead of the old chat interface. The OpenAI model integration remains separate from the legacy OpenAI agent package and is still needed by applications that use an OpenAI model.
A small offline regression case can separate agent execution changes from model variability. A replay model based on LlamaIndex's MockLLM requests multiplication, and the real Python tool supplies the answer. This tests tool dispatch and response handling without an API key; it does not establish how a hosted model will choose tools or preserve an existing conversation.
Steps to migrate legacy LlamaIndex agents to FunctionAgent:
- Create an isolated directory for the migration comparison.
$ mkdir agent-migration
Tested with Python 3.12, 3.13, and 3.14. The comparison uses a legacy AgentRunner implementation through FunctionCallingAgent. An existing application's source, dependency lock, and working environment should remain available until its own regression tests pass.
- Change into the migration comparison directory.
$ cd agent-migration
- Create the legacy Python environment.
$ python3 -m venv .venv-legacy
On Debian or Ubuntu, an ensurepip error means that the python3-venv package is missing.
Related: How to create a virtual environment in Python - Install the legacy agent dependencies in that environment.
$ .venv-legacy/bin/python -m pip install \ "llama-index-core==0.12.52" \ "llama-index-agent-openai==0.4.12"
These pins reproduce the old API for comparison. The legacy agent package requires core below 0.13; installing it alongside current core creates a dependency conflict. The example agent comes from core and never imports that package; an OpenAIAgent import does. An application installed through the 0.12 llama-index package has the same legacy package and follows the same steps, tested on Python 3.12 and 3.13.
- Create migration_case.py in your text editor with the shared input and multiplication tool.
- migration_case.py
import re from llama_index.core.llms import ( ChatMessage, ChatResponse, LLMMetadata, ) from llama_index.core.llms.function_calling import ( FunctionCallingLLM, ) from llama_index.core.llms.llm import ToolSelection from llama_index.core.llms.mock import MockLLM from llama_index.core.tools import FunctionTool PROMPT = "What is 6 * 7?" calls = [] def multiply(a: int, b: int) -> int: """Multiply two integers.""" calls.append((a, b)) return a * b tool = FunctionTool.from_defaults(fn=multiply)
- Append the replay model to migration_case.py.
class ReplayLLM(MockLLM, FunctionCallingLLM): @property def metadata(self): return LLMMetadata( is_chat_model=True, is_function_calling_model=True ) def _prepare_chat_with_tools( self, tools, user_msg=None, chat_history=None, **kwargs ): messages = list(chat_history or []) if user_msg is not None: messages.append( ChatMessage(role="user", content=str(user_msg)) ) return {"messages": messages} def chat(self, messages, **kwargs): if messages[-1].role == "tool": return ChatResponse(message=ChatMessage( role="assistant", content=messages[-1].content)) question = next( m.content for m in reversed(messages) if m.role == "user" ) match = re.search(r"(\d+)\s*\*\s*(\d+)", question) a, b = map(int, match.groups()) selection = ToolSelection( tool_id="multiply-1", tool_name="multiply", tool_kwargs={"a": a, "b": b}, ) return ChatResponse(message=ChatMessage( role="assistant", additional_kwargs={"tool_calls": [selection]}, )) def get_tool_calls_from_response(self, response, **kwargs): return response.message.additional_kwargs.get( "tool_calls", [] ) async def astream_chat(self, messages, **kwargs): async def responses(): yield self.chat(messages, **kwargs) return responses()
The replay model extracts the operands from the question and requests the named tool. Its final message comes from the tool response, rather than a prewritten answer. FunctionAgent reads the model through the asynchronous streaming method on both core releases, so the class provides that method as well as chat.
- Append the regression checks to migration_case.py.
def check(answer): assert calls == [(6, 7)], calls assert str(answer).strip() == "42", answer print(f"multiply arguments: {calls}") print(f"answer: {answer}")
A missing call, changed arguments, duplicate invocation, or different answer raises AssertionError. The model can request multiplication, but only execution of the Python tool populates the call list.
- Save migration_case.py.
- Create agent.py in your text editor with the legacy agent entry point.
- agent.py
from llama_index.core.agent import FunctionCallingAgent from migration_case import PROMPT, ReplayLLM, check, tool agent = FunctionCallingAgent.from_tools( tools=[tool], llm=ReplayLLM() ) response = agent.chat(PROMPT) check(response)
- Save agent.py.
- Run the legacy agent to establish the expected tool call and answer.
$ .venv-legacy/bin/python agent.py ##### snipped ##### multiply arguments: [(6, 7)] answer: 42
The old agent emits deprecation warnings and a pydantic warning. Those warnings are separate from the two result lines and do not mean that the assertions failed.
- Preserve the legacy entry point for rollback.
$ cp agent.py agent.legacy.py
- Freeze the complete legacy dependency set.
$ .venv-legacy/bin/python -m pip freeze \ > requirements.legacy.txt
For an existing application, this snapshot records the working environment for rollback, including its own dependencies and integrations.
- Open agent.py in your text editor.
- Replace the legacy agent import with the workflow agent import and the asyncio import.
import asyncio from llama_index.core.agent.workflow import FunctionAgent
The replaced line is the import of FunctionCallingAgent. The import from migration_case remains in place. An OpenAIAgent application similarly replaces its legacy agent import while retaining its model and tool setup.
- Replace the agent = FunctionCallingAgent.from_tools(...) statement with the FunctionAgent constructor.
agent = FunctionAgent(tools=[tool], llm=ReplayLLM())
FunctionAgent accepts tools and a function-calling model directly. An application's existing compatible model instance belongs in the llm argument; the OpenAI model integration is not replaced by the replay model in production.
- Replace the final response = agent.chat(PROMPT) and check(response) lines with asynchronous execution.
async def main(): result = await agent.run(PROMPT) check(result) if __name__ == "__main__": asyncio.run(main())
Converting the workflow result to a string gives the answer text that check compares. In a notebook or an asynchronous web handler, the existing event loop calls await main() instead of asyncio.run(main()). Conversation history and AgentRunner task APIs need their own migration; this single-turn comparison does not carry them across.
- Save the migrated agent.py.
- Run FunctionAgent on the legacy dependency set.
$ .venv-legacy/bin/python agent.py ##### snipped ##### multiply arguments: [(6, 7)] answer: 42
The snipped output is the same harmless pydantic warning. Both implementations must call multiply once with 6 and 7 and return 42 before the dependency changes begin.
- Create a new environment for the migrated agent.
$ python3 -m venv .venv-migrate
- Install llama-index 0.14.25 and your application's other dependencies in the new environment.
$ .venv-migrate/bin/python -m pip install "llama-index==0.14.25"
An application's other packages go in this command without their old pins, and llama-index-agent-openai stays out. llama-index 0.14.25 requires llama-index-llms-openai below 0.8, so a higher pin conflicts with it. Upgrading the old environment in place keeps 0.12 packages such as llama-index-agent-openai, and pip check reports them as broken.
Related: How to install LlamaIndex with pip - Check the new dependency graph for conflicts.
$ .venv-migrate/bin/python -m pip check No broken requirements found.
- Confirm that the legacy agent distribution is absent.
$ .venv-migrate/bin/python -m pip show llama-index-agent-openai WARNING: Package(s) not found: llama-index-agent-openai
pip reports the package as not found and exits nonzero, which is expected for this absence check. Legacy OpenAIAgent imports fail in this environment, while the original environment and agent.legacy.py remain available for rollback.
- Write the migrated dependency lock for future installs.
$ .venv-migrate/bin/python -m pip freeze \ > requirements.migrated.txt
The migrated lock replaces the legacy lock when the application is switched to the new environment. Reinstalling requirements.legacy.txt would restore the legacy package and old pins.
- Rerun the migrated agent on the new dependency set.
$ .venv-migrate/bin/python agent.py multiply arguments: [(6, 7)] answer: 42
The same assertions now exercise FunctionAgent without the legacy package. The answer is the tool result relayed by the replay model. Both result lines must match the legacy run, and the process must exit successfully. A traceback or changed call trace means that the migration has not preserved the tested task.