Multi Turn Assistant Ollama
Multi turn assistant ollama example.
Multi-Turn AI Assistant with MCP Tool Integration using Ollama.
Run it
python examples/ai/multi_turn_assistant_ollama.py
"""
Multi-Turn AI Assistant with MCP Tool Integration using Ollama.
This example demonstrates a multi-turn conversational AI assistant that can autonomously
use tools via the Model Context Protocol (MCP). The assistant decides when to use tools
versus when to respond directly to the user, creating a natural conversation flow.
Key Features:
- **Natural Tool Calling**: LLM autonomously decides when to use tools vs respond
- **Multi-Turn Conversations**: Maintains context across multiple interactions
- **Dynamic Tool Discovery**: Discovers available tools from MCP servers at runtime
- **Multi-MCP Support**: Can connect to multiple MCP servers simultaneously
- **Autonomous Reasoning**: Assistant decides its own approach to problems
Prerequisites:
1. Install Ollama: https://ollama.ai
2. Pull a model that supports tools: ollama pull llama3.2
3. Start Ollama service: ollama serve
4. Start Flux server: poetry run flux start server
5. Start Flux worker: poetry run flux start worker worker-1
6. Start Flux MCP server: poetry run flux start mcp
Usage:
# Ask the assistant to help with workflows
flux workflow run multi_turn_assistant_ollama '{"message": "What workflows are available?"}'
# Continue the conversation
flux workflow resume multi_turn_assistant_ollama <execution_id> '{"message": "Run the hello_world workflow"}'
# Use multiple MCP servers (e.g., Flux + web search)
flux workflow run multi_turn_assistant_ollama '{
"message": "Search for best practices in distributed systems",
"mcp_urls": ["http://localhost:8080/mcp", "http://localhost:8081/mcp"]
}'
Example Conversation Flow:
User: "List available workflows and run the hello_world one"
Assistant: *calls list_workflows*
Assistant: *sees results, calls execute_workflow_async*
Assistant: *sees execution started*
Assistant: "I found 5 workflows. I've started the hello_world workflow
with execution ID abc-123. It should complete shortly."
User: "What's the status?"
Assistant: *calls get_execution_status*
Assistant: "The workflow completed successfully! Output: Hello, World!"
"""
from __future__ import annotations
import json
from typing import Any
from fastmcp import Client
from ollama import AsyncClient
from flux import ExecutionContext, task, workflow
from flux.tasks import pause
# =============================================================================
# MCP Integration - Dynamic Tool Discovery
# =============================================================================
@task
async def discover_tools_from_mcp_servers(
mcp_urls: list[str],
) -> tuple[list[dict[str, Any]], dict[str, str]]:
"""
Connect to multiple MCP servers and discover all available tools.
Args:
mcp_urls: List of MCP server URLs
Returns:
Tuple of (tool schemas, tool_to_server_mapping)
"""
all_tools = []
tool_to_server = {}
failed_servers = []
for mcp_url in mcp_urls:
try:
async with Client(mcp_url) as client:
tools = await client.list_tools()
for tool in tools:
tool_schema = {
"name": tool.name,
"description": tool.description,
"input_schema": tool.inputSchema,
}
all_tools.append(tool_schema)
tool_to_server[tool.name] = mcp_url
except Exception as e:
failed_servers.append(mcp_url)
print(f"Warning: Could not connect to MCP server at {mcp_url}: {str(e)}")
continue
if not all_tools:
error_msg = "Could not discover any tools from MCP servers. "
if failed_servers:
error_msg += f"\nFailed servers: {', '.join(failed_servers)}"
error_msg += "\n\nMake sure at least one MCP server is running:"
error_msg += "\n poetry run flux start mcp"
raise RuntimeError(error_msg)
return all_tools, tool_to_server
@task
async def convert_mcp_tools_to_ollama(mcp_tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""
Convert MCP tool schemas to Ollama function calling format.
Args:
mcp_tools: List of MCP tool schemas
Returns:
List of tools in Ollama format
"""
ollama_tools = []
for tool in mcp_tools:
ollama_tool = {
"type": "function",
"function": {
"name": tool["name"],
"description": tool["description"],
"parameters": tool["input_schema"],
},
}
ollama_tools.append(ollama_tool)
return ollama_tools
@task.with_options(retry_max_attempts=3, retry_delay=1, retry_backoff=2, timeout=60)
async def execute_mcp_tool(
mcp_url: str,
tool_name: str,
tool_args: dict[str, Any],
) -> str:
"""
Execute a tool via MCP protocol.
Args:
mcp_url: URL of the MCP server that hosts this tool
tool_name: Name of the tool to execute
tool_args: Arguments for the tool
Returns:
JSON string with tool execution result
"""
try:
async with Client(mcp_url) as client:
result = await client.call_tool(tool_name, tool_args)
if result and len(result) > 0:
content = result[0]
if hasattr(content, "text"):
return content.text
elif isinstance(content, dict) and "text" in content:
return content["text"]
else:
return json.dumps({"result": str(content)})
else:
return json.dumps({"result": "Tool executed successfully but returned no content"})
except Exception as e:
error_message = f"Failed to execute tool '{tool_name}': {str(e)}"
return json.dumps({"error": error_message, "success": False})
# =============================================================================
# LLM Integration
# =============================================================================
@task.with_options(retry_max_attempts=3, retry_delay=1, retry_backoff=2, timeout=90)
async def call_ollama_with_tools(
messages: list[dict[str, Any]],
system_prompt: str,
model: str,
ollama_url: str,
ollama_tools: list[dict[str, Any]],
) -> dict[str, Any]:
"""
Call Ollama API with tools for multi-turn reasoning.
Args:
messages: Conversation history
system_prompt: System instructions for the assistant
model: Ollama model name (must support tools, e.g., llama3.2)
ollama_url: Ollama server URL
ollama_tools: List of tools in Ollama format
Returns:
Response dict with 'message' and optional 'tool_calls'
"""
try:
client = AsyncClient(host=ollama_url)
full_messages = [{"role": "system", "content": system_prompt}]
full_messages.extend(messages)
response = await client.chat(
model=model,
messages=full_messages,
tools=ollama_tools,
)
return response
except Exception as e:
raise RuntimeError(
f"Failed to call Ollama API: {str(e)}. "
"Make sure Ollama is running (ollama serve) and the model supports tools (e.g., llama3.2).",
) from e
# =============================================================================
# Main Workflow
# =============================================================================
@workflow
async def multi_turn_assistant_ollama(ctx: ExecutionContext[dict[str, Any]]):
"""
Multi-turn AI assistant with autonomous tool use via MCP.
The assistant uses a natural tool calling pattern where it autonomously
decides when to use tools versus when to respond to the user. This creates
a fluid conversation where the assistant can chain multiple tool calls
together to complete complex tasks.
Initial Input format:
{
"message": "User's request or question",
"system_prompt": "Optional custom system prompt",
"model": "llama3.2", # or "mistral" (must support tools)
"ollama_url": "http://localhost:11434", # Ollama server URL
"mcp_urls": [ # List of MCP servers
"http://localhost:8080/mcp", # Flux MCP server
"http://localhost:8081/mcp" # Optional: additional MCP servers
],
"max_turns": 20 # Maximum conversation turns
}
Resume Input format:
{
"message": "User's next message or question"
}
"""
# Get initial configuration
initial_input = ctx.input or {}
system_prompt = initial_input.get(
"system_prompt",
"""You are a helpful AI assistant with access to tools via MCP.
Your approach:
- When users ask questions, think about what information you need
- Use tools when you need real-time data or to perform actions
- You can chain multiple tool calls together to complete complex tasks
- Explain your reasoning and what you're doing
Tool usage guidelines:
- Call tools when you need current information (workflows, executions, etc.)
- You can call multiple tools in sequence - keep using tools until you have enough information
- Once you have what you need, provide a clear response to the user
Be helpful, clear, and proactive in using tools to assist users.""",
)
model = initial_input.get("model", "llama3.2")
ollama_url = initial_input.get("ollama_url", "http://localhost:11434")
mcp_urls = initial_input.get(
"mcp_urls",
["http://localhost:8080/mcp"], # Default to Flux MCP server
)
max_turns = initial_input.get("max_turns", 20)
# Discover tools from all MCP servers
mcp_tools, tool_to_server = await discover_tools_from_mcp_servers(mcp_urls)
ollama_tools = await convert_mcp_tools_to_ollama(mcp_tools)
# Initialize conversation state
messages: list[dict[str, Any]] = []
tools_used: list[dict[str, Any]] = []
# Process first message
first_message = initial_input.get("message", "")
if not first_message:
return {
"error": "No message provided in initial input",
"execution_id": ctx.execution_id,
"available_tools": [t["name"] for t in mcp_tools],
}
messages.append({"role": "user", "content": first_message})
# Main conversation loop
for turn in range(max_turns):
# Call LLM - it decides whether to use tools or respond
response = await call_ollama_with_tools(
messages=messages,
system_prompt=system_prompt,
model=model,
ollama_url=ollama_url,
ollama_tools=ollama_tools,
)
message = response["message"]
# Natural tool calling loop: keep using tools until LLM responds
while message.get("tool_calls"):
# Execute all requested tool calls
for tool_call in message["tool_calls"]:
tool_name = tool_call["function"]["name"]
tool_args = tool_call["function"]["arguments"]
# Find which MCP server hosts this tool
mcp_url = tool_to_server.get(tool_name)
if not mcp_url:
tool_result = json.dumps(
{"error": f"Tool '{tool_name}' not found in any MCP server"},
)
else:
# Execute tool via appropriate MCP server
tool_result = await execute_mcp_tool(mcp_url, tool_name, tool_args)
# Track tool usage
tools_used.append(
{
"tool": tool_name,
"args": tool_args,
"turn": turn + 1,
},
)
# Add tool result to conversation
messages.append(
{
"role": "tool",
"content": tool_result,
},
)
# Call LLM again with tool results - it decides what to do next
response = await call_ollama_with_tools(
messages=messages,
system_prompt=system_prompt,
model=model,
ollama_url=ollama_url,
ollama_tools=ollama_tools,
)
message = response["message"]
# LLM returned content without tool calls - ready to respond to user
assistant_content = message.get("content", "")
if assistant_content:
messages.append({"role": "assistant", "content": assistant_content})
# Pause and wait for next user input, returning the assistant's response
resume_input = await pause(
f"waiting_for_user_input_turn_{turn + 1}",
output=assistant_content,
)
# Get next message
next_message = resume_input.get("message", "") if resume_input else ""
if not next_message:
return {
"conversation_history": messages,
"total_turns": turn + 1,
"tools_used": tools_used,
"execution_id": ctx.execution_id,
"available_tools": [t["name"] for t in mcp_tools],
}
messages.append({"role": "user", "content": next_message})
# Max turns reached
return {
"conversation_history": messages,
"total_turns": max_turns,
"tools_used": tools_used,
"message": "Maximum conversation turns reached",
"execution_id": ctx.execution_id,
"available_tools": [t["name"] for t in mcp_tools],
}
if __name__ == "__main__": # pragma: no cover
# Quick test of the workflow
print("=" * 80)
print("Multi-Turn Assistant Demo - Natural Tool Calling with MCP")
print("=" * 80 + "\n")
try:
# Test: Ask assistant to discover and explain workflows
print("Test: Ask assistant about available workflows\n")
result = multi_turn_assistant_ollama.run(
{
"message": "What workflows are available? Pick one and tell me what it does.",
"model": "llama3.2",
},
)
if result.has_failed:
raise Exception(f"Test failed: {result.output}")
conversation = result.output.get("conversation_history", [])
if conversation:
print(f"Assistant: {conversation[-1].get('content', 'No response')}\n")
tools_used = result.output.get("tools_used", [])
if tools_used:
print(f"\nTools used: {len(tools_used)}")
for tool_use in tools_used:
print(f" - {tool_use['tool']} (turn {tool_use['turn']})")
print("\n" + "=" * 80)
print("Multi-Turn Assistant working successfully!")
print("Natural tool calling pattern demonstrated!")
print("=" * 80)
except Exception as e:
print(f"Error: {e}")
print("\nMake sure all services are running:")
print("1. Flux server: poetry run flux start server")
print("2. Flux worker: poetry run flux start worker worker-1")
print("3. Flux MCP server: poetry run flux start mcp")
print("4. Ollama: ollama serve")
print("5. Ollama model: ollama pull llama3.2")
Last verified against Flux 0.56.0.