본문 바로가기
AI 도구

🛠️ Ollama Tool 기능 가이드: 로컬 AI로 날씨 조회부터 웹 검색까지

by James AI Explorer 2024. 7. 28.
    728x90

    안녕하세요! 오늘은 최근 업데이트Ollama의 Tool 기능에 대해 살펴보겠습니다. Tool 지원 기능은 AI 모델들이 더 복잡한 작업을 수행하고 외부 세계와 상호 작용할 수 있도록 해주는 역할을 하며, Ollama는 Llama 3.1, Mistral Nemo와 같은 최신 인기 AI 모델을 통해 다양한 도구를 호출할 수 있도록 지원하고, 이를 통해 모델은 주어진 프롬프트에 대해 알고 있는 도구를 사용하여 더욱 정확하고 유용한 답변을 제공할 수 있습니다. 이 블로그에서는 Ollama Tool 기능의 개요와 지원모델에 대해 알아보고, 로컬 AI 모델, Mistral Nemo를 통해 날씨와 인터넷 정보를 검색하는 예제를 구현해 보겠습니다. 

    🛠️ Ollama Tool 기능 가이드: 로컬 AI로 날씨 조회부터 웹 검색까지

    https://ollama.com/blog/tool-support

     

    Tool support · Ollama Blog

    Ollama now supports tool calling with popular models such as Llama 3.1. This enables a model to answer a given prompt using tool(s) it knows about, making it possible for models to perform more complex tasks or interact with the outside world.

    ollama.com

    "이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."

    Ollama Tool 개요

    Ollama의 Tool 지원은 단순한 정보 제공을 넘어, 사용자가 필요로 하는 특정 기능을 수행할 수 있도록 돕습니다. 예를 들어, 일반 언어 모델이 할 수 없는 현재 날씨를 확인하거나, 웹 브라우징, 특정 코드 해석기를 통한 코드 분석 등 다양한 함수와 API를 호출하는 기능을 수행하도록 할 수 있습니다. 이는 모델의 응답을 더욱 정교하게 만들고, 사용자에게 보다 유용한 정보를 제공하도록 지원합니다. 

     

    Tool 기능을 활용한 유용한 작업의 종류는 다음과 같습니다.

    • 날씨정보 확인: 날씨 API를 호출하여 현재날씨, 기상예보 등을 확인할 수 있습니다.
    • 웹 브라우징: Duckduckgo 인터넷 검색으로 최신 정보를 검색할 수 있습니다. 
    • 계산기: 수학 계산기능을 호출하여 실행할 수 있습니다.

    Ollama Tool 지원 모델 확인

    Ollama Tool 지원을 제공하는 모델은 위 화면과 같이 모델 검색화면에서 Tools를 클릭하거나, 아래 링크에서 확인할 수 있습니다. 현재 Llama 3.1, Mistral Nemo, Command-R + 등 다양한 모델이 Ollama Tool을 지원하며, 모델은 특정 도구와 결합되어 사용자에게 최적의 성능을 제공하고, 사용자는 Ollama API를 통해 손쉽게 이 모델들을 활용할 수 있습니다.

    https://ollama.com/search?c=tools

     

    Ollama

    Get up and running with large language models.

    ollama.com

    728x90

    Ollama Tool 예제

    다음은 Ollama Tool 기능을 활용한 예제를 만들어 보겠습니다. 예제는 Gradio를 사용하여 웹 인터페이스를 만들고, Ollama AI 모델, Mistral NeMo를 활용해 사용자 질문에 답변합니다. 날씨 정보 조회, DuckDuckGo 검색, 한영 번역 기능을 제공하며, OpenWeather API로 실시간 날씨 데이터를 가져옵니다. 사용자는 텍스트 입력이나 미리 정의된 버튼으로 질문할 수 있고, 시스템은 적절한 응답을 생성하여 표시합니다.

     

    이 블로그의 작업 환경은 Windows 11 Pro(23H2), WSL2, 파이썬 버전 3.11, 비주얼 스튜디오 코드(이하 VSC) 1.90.2이며, VSC를 실행하여 "WSL 연결"을 통해 Windows Subsystem for Linux(WSL) Linux 환경에 액세스 하도록 구성하였습니다. 작업순서는 다음과 같습니다.

    Ollama 설치 및 모델 다운로드

    1. Ollama 모델 다운로드: Ollama 설치 후, Tool 지원 모델 중 한국어를 지원하는 Mistral Nemo 모델을 다운로드합니다. 

    Mistral Nemo 모델 을 다운로드

    환경설정

    2. 가상환경 생성 및 활성화: VSC 프롬프트 메인 디렉토리에서 다음 명령어를 통해 가상환경을 만들고 활성화합니다.

    python3.11 -m venv myenv
    source myenv/bin/activate

    3. 의존성 패키지 설치: 가상환경이 활성화된 상태에서 아래 명령어로 의존성을 설치합니다.

    pip install gradio ollama deep-translator requests duckduckgo-search

    파이썬 의존성 패키지 설치

    4. OpenWeather API KEY 발급: 아래 사이트에서 날씨정보 검색을 위한 OpenWeather API KEY를 발급합니다. 

    https://home.openweathermap.org/api_keys

     

    Members

    Enter your email address and we will send you a link to reset your password.

    home.openweathermap.org

    코드작성 및 실행

    5. 코드 작성: VSC에서 새 파이썬 파일을 만들고 아래 코드를 복사해서 붙여 넣고, OpenWeather API KEY를 입력한 후, app.py로 저장합니다. 이 코드는 Gradio를 사용하여 날씨 정보 및 인터넷 검색 결과를 제공하는 인터페이스를 구성합니다. 사용자는 검색 쿼리를 입력하거나 미리 정의된 버튼을 클릭하여 자동으로 쿼리를 설정하고, 그에 대한 응답을 출력 박스에서 확인할 수 있습니다. `Submit` 버튼 클릭이나 엔터 키를 통해 쿼리를 실행하고, `Clear` 버튼을 사용해 입력 및 응답 필드를 비울 수 있습니다.

    import os
    import gradio as gr
    import ollama
    from deep_translator import GoogleTranslator
    from datetime import datetime, timedelta
    import requests
    import re
    from duckduckgo_search import DDGS
    
    # 환경 변수를 통해 API 키를 가져옵니다.
    os.environ["OPENWEATHER_API_KEY"] = "발급받은 API KEY"  # OpenWeather API 키
    
    # DuckDuckGo 검색 함수
    def duckduckgo_search(query: str, max_results: int = 5):
        """Perform a DuckDuckGo search and return results."""
        params = {
            "keywords": query,
            "max_results": int(max_results),
        }
    
        results = []
        with DDGS() as ddg:
            for result in ddg.text(**params):
                results.append({
                    'title': result['title'],
                    'body': result['body']
                })
    
        return results
    
    # 현재 날씨 정보를 가져오는 함수
    def get_current_weather(city):
        api_key = os.getenv('OPENWEATHER_API_KEY')  # 환경 변수에서 API 키를 읽어옵니다.
        base_url = 'http://api.openweathermap.org/data/2.5/weather'
        params = {
            'q': city,
            'appid': api_key,
            'units': 'metric'
        }
        response = requests.get(base_url, params=params)
        data = response.json()
    
        if response.status_code == 200:
            weather = {
                'temperature': f"{data['main']['temp']}°C",
                'condition': data['weather'][0]['description'].capitalize(),
                'humidity': f"{data['main']['humidity']}%",
                'wind_speed': f"{data['wind']['speed']} m/s"
            }
            return weather
        else:
            return {'error': '날씨 데이터를 가져올 수 없습니다.'}
    
    # 48시간 예보를 가져오는 함수
    def get_hourly_forecast(city):
        api_key = os.getenv('OPENWEATHER_API_KEY')
        base_url = 'http://api.openweathermap.org/data/2.5/forecast'
        params = {
            'q': city,
            'appid': api_key,
            'units': 'metric'
        }
        response = requests.get(base_url, params=params)
        data = response.json()
    
        if response.status_code == 200:
            hours = data['list'][:16]  # 48시간의 예보(3시간 간격)
            forecast = []
            for hour in hours:
                # UTC 시간을 한국 시간으로 변환
                utc_time = datetime.strptime(hour['dt_txt'], '%Y-%m-%d %H:%M:%S')  # 데이터를 datetime 객체로 변환
                korean_time = utc_time + timedelta(hours=9)  # UTC 시간에 9시간 추가
    
                forecast_entry = {
                    'time': korean_time.strftime('%Y-%m-%d %H:%M:%S'),  # 한국 시간으로 포매팅
                    'temperature': f"{hour['main']['temp']}°C",
                    'condition': translate_to_korean(hour['weather'][0]['description']),
                    'humidity': f"{hour['main']['humidity']}%",
                    'wind_speed': f"{hour['wind']['speed']} m/s"
                }
                forecast.append(forecast_entry)
            return forecast
        else:
            return {'error': '시간별 예보 데이터를 가져올 수 없습니다.'}
    
    # 영어 답변을 한국어로 번역하는 함수
    def translate_to_korean(text: str) -> str:
        """Translate English text to Korean using deep-translator."""
        translated = GoogleTranslator(source='en', target='ko').translate(text)
        return translated
    
    # 한국어를 영어로 번역하는 함수
    def translate_to_english(text: str) -> str:
        """Translate Korean text to English using deep-translator."""
        translated = GoogleTranslator(source='ko', target='en').translate(text)
        return translated
    
    def is_korean(text):
        return bool(re.search('[가-힣]', text))
    
    def extract_city_name(text):
        # Ollama 모델을 사용하여 도시 이름 추출
        prompt = f"""다음 텍스트에서 도시 이름을 추출하세요: '{text}'.
        1. 서울의 경우 '특별시'를 추가해주세요. (예: '서울' -> '서울특별시')
        2. 부산, 대구, 인천, 광주, 대전, 울산의 경우 반드시 '광역시'를 추가해주세요. (예: '부산' -> '부산광역시')
        3. 그 외의 모든 도시는 이름 뒤에 '시'를 붙여주세요. (예: '포항' -> '포항시', '화성' -> '화성시')
        4. 이미 '시', '군', '구'로 끝나는 도시 이름은 그대로 유지해주세요.
        5. 도시 이름만 간단히 반환해주세요.
        6. 도시 이름이 없으면 '없음'이라고 반환해주세요.
        7. 반드시 위의 규칙을 따라 도시 이름을 정확히 반환해주세요.
        """
        
        response = ollama.chat(
            model='mistral-nemo',
            messages=[{'role': 'user', 'content': prompt}]
        )
        
        extracted_city = response['message']['content'].strip()
        
        # 도시 이름이 추출되지 않았을 경우 기본값 반환
        if not extracted_city or extracted_city.lower() == "없음":
            return None
        
        # 서울특별시 처리
        if extracted_city == "서울" or extracted_city == "서울시":
            return "서울특별시"
        
        # 광역시 처리
        metropolitan_cities = ['부산', '대구', '인천', '광주', '대전', '울산']
        for city in metropolitan_cities:
            if city in extracted_city and not extracted_city.endswith('광역시'):
                return f"{city}광역시"
        
        # 나머지 도시 처리
        if not extracted_city.endswith(('시', '군', '구')):
            return f"{extracted_city}시"
        
        return extracted_city
    
    
    # 검색 결과를 요약하는 함수 추가
    def summarize_search_results(results):
        combined_text = "\n\n".join([f"제목: {result['title']}\n내용: {result['body']}" for result in results])
        
        # Ollama를 사용하여 요약 생성
        summary_prompt = f"다음 검색 결과들을 한국어로 간단히 요약하고 출처 url을 보여주세요:\n\n{combined_text}\n\n요약:"
        summary_response = ollama.chat(
            model='mistral-nemo',
            messages=[{'role': 'user', 'content': summary_prompt}]
        )
        
        return summary_response['message']['content']
    
    # 대화를 실행하는 함수
    def run_conversation(user_prompt: str) -> str:
        # 한국어로 입력된 경우 처리
        if is_korean(user_prompt):
            if "날씨" in user_prompt or "예보" in user_prompt or "온도" in user_prompt:
                city = extract_city_name(user_prompt)
                print(f"추출된 도시 이름: {city}")  # 디버깅을 위한 출력
    
                if city is None:
                    return "죄송합니다. 도시 이름을 인식할 수 없습니다. 도시 이름을 명확히 말씀해 주세요."
    
                if "시간대별" in user_prompt or "예보" in user_prompt:
                    forecast_info = get_hourly_forecast(city)
                    if 'error' not in forecast_info:
                        forecast_output = f"🌤️ {city}의 48시간 날씨 예보:\n"
                        for hour in forecast_info:
                            forecast_output += (
                                f"시간: {hour['time']}, "
                                f"온도: {hour['temperature']}, "
                                f"날씨 상태: {hour['condition']}, "
                                f"습도: {hour['humidity']}, "
                                f"풍속: {hour['wind_speed']}\n"
                            )
                        return forecast_output
                    else:
                        return f"{city}의 날씨 정보를 가져오는 데 실패했습니다. 도시 이름을 확인해 주세요."
    
                else:
                    weather_info = get_current_weather(city)
                    if 'error' not in weather_info:
                        weather_output = (
                            f"🌤️ 현재 날씨 정보:\n"
                            f"도시: {city}\n"
                            f"온도: {weather_info['temperature']}\n"
                            f"날씨 상태: {weather_info['condition']}\n"
                            f"습도: {weather_info['humidity']}\n"
                            f"풍속: {weather_info['wind_speed']}\n"
                        )
                        return weather_output
                    else:
                        return f"{city}의 날씨 정보를 가져오는 데 실패했습니다. 도시 이름을 확인해 주세요."
            else:
                # DuckDuckGo 검색 수행 및 요약
                search_results = duckduckgo_search(user_prompt)
                summary = summarize_search_results(search_results)  # 요약할 때 한국어로 요청됨
                return f"검색 결과 요약:\n\n{summary}"  # 번역 없이 그대로 응답
    
        user_prompt_translated = translate_to_english(user_prompt) if is_korean(user_prompt) else user_prompt
    
        response = ollama.chat(
            model='mistral-nemo',
            messages=[{'role': 'user', 'content': user_prompt_translated}],
            tools=[
                {
                    'type': 'function',
                    'function': {
                        'name': 'get_current_weather',
                        'description': 'Get the current weather for a city',
                        'parameters': {
                            'type': 'object',
                            'properties': {
                                'city': {
                                    'type': 'string',
                                    'description': 'The name of the city',
                                },
                            },
                            'required': ['city'],
                        },
                    },
                },
                {
                    'type': 'function',
                    'function': {
                        'name': 'duckduckgo_search',
                        'description': 'Perform a DuckDuckGo search',
                        'parameters': {
                            'type': 'object',
                            'properties': {
                                'query': {
                                    'type': 'string',
                                    'description': 'The search query to execute.',
                                },
                                'max_results': {
                                    'type': 'integer',
                                    'description': 'The maximum number of search results to return.',
                                    'default': 5,
                                },
                            },
                            'required': ['query'],
                        },
                    },
                },
                {
                    'type': 'function',
                    'function': {
                        'name': 'get_hourly_forecast',
                        'description': 'Get the hourly weather forecast for a city',
                        'parameters': {
                            'type': 'object',
                            'properties': {
                                'city': {
                                    'type': 'string',
                                    'description': 'The name of the city',
                                },
                            },
                            'required': ['city'],
                        },
                    },
                }
            ],
        )
    
        tool_calls = response['message'].get('tool_calls', [])
    
        if tool_calls:
            results = []
            for tool_call in tool_calls:
                function_name = tool_call['function']['name']
                arguments = tool_call['function']['arguments']
                if function_name == 'get_current_weather':
                    city = arguments.get('city')
                    if city:
                        weather_info = get_current_weather(city)
                        if 'error' not in weather_info:
                            weather_output = (
                                f"🌤️ 현재 날씨 정보:\n"
                                f"도시: {city}\n"
                                f"온도: {weather_info['temperature']}\n"
                                f"날씨 상태: {weather_info['condition']}\n"
                                f"습도: {weather_info['humidity']}\n"
                                f"풍속: {weather_info['wind_speed']}\n"
                            )
                            results.append(weather_output)
                        else:
                            results.append(weather_info['error'])
                elif function_name == 'duckduckgo_search':
                    query = arguments.get('query')
                    max_results = arguments.get('max_results', 5)
                    if query:
                        search_results = duckduckgo_search(query, max_results)
                        summary = summarize_search_results(search_results)
                        results.append(f"검색 결과 요약:\n\n{summary}")
                elif function_name == 'get_hourly_forecast':
                    city = arguments.get('city')
                    if city:
                        forecast_info = get_hourly_forecast(city)
                        if 'error' not in forecast_info:
                            forecast_output = f"🌤️ {city}의 48시간 날씨 예보:\n"
                            for hour in forecast_info:
                                forecast_output += (
                                    f"시간: {hour['time']}, "
                                    f"온도: {hour['temperature']}, "
                                    f"날씨 상태: {hour['condition']}, "
                                    f"습도: {hour['humidity']}, "
                                    f"풍속: {hour['wind_speed']}\n"
                                )
                            results.append(forecast_output)
                        else:
                            results.append(forecast_info['error'])
            return "\n\n".join(results)
    
        else:
            return "도구 호출이 없습니다."  # 도구 호출이 발견되지 않았을 때 메시지
    
    # Gradio 인터페이스 정의
    def process_query(query: str) -> str:
        # 입력이 None이거나 공백인 경우 처리
        if query is None or query.strip() == "":
            return "입력 내용이 없습니다. 검색어를 입력해주세요."
        return run_conversation(query)
    
    def on_button_click(query):
        return run_conversation(query)
    
    title = "Ollama Mistral-Nemo 인터넷 검색 및 국내 날씨 정보"
    
    with gr.Blocks() as iface:
        gr.Markdown(f"<h1 style='text-align: center;'>{title}</h1>")
        gr.Markdown("질문을 입력하면 Ollama AI가 DuckDuckGo 인터넷 검색 및 현재 날씨 정보를 응답합니다.")
    
        inputs = gr.Textbox(label="검색 쿼리", placeholder="대/중소 도시명('서울', '대전', '하남' 등)과 함께 날씨정보를 요청하거나 검색어를 입력하세요.", interactive=True)  
    
        # 미리 정의된 쿼리 버튼들
        button1 = gr.Button("서울특별시 날씨 알려줘")
        button2 = gr.Button("하남시 시간대별 날씨 알려줘")
        button3 = gr.Button("갤럭시 Z 플립6 검색해줘")
    
        output_box = gr.Textbox(label="응답", interactive=False)
    
        # 각 버튼 클릭 이벤트 처리
        button1.click(fn=on_button_click, inputs=gr.Textbox(value="서울특별시 날씨 알려줘", visible=False), outputs=output_box)
        button2.click(fn=on_button_click, inputs=gr.Textbox(value="하남시 시간대별 날씨 알려줘", visible=False), outputs=output_box)
        button3.click(fn=on_button_click, inputs=gr.Textbox(value="갤럭시 Z 플립6 검색해줘", visible=False), outputs=output_box)
    
        # 입력된 쿼리에 대해 Submit 버튼 (variant="primary"로 설정)
        submit_button = gr.Button("Submit", variant="primary")
        submit_button.click(fn=process_query, inputs=inputs, outputs=output_box)
    
        # 입력 필드에서 Enter 키를 눌렀을 때 이벤트
        inputs.submit(fn=process_query, inputs=inputs, outputs=output_box)
    
        # Clear 버튼 추가 (모든 텍스트를 비우는 기능)
        def clear_inputs():
            return "", ""  # inputs와 outputs 모두 빈 문자열로 반환
    
        clear_button = gr.Button("Clear")
        clear_button.click(fn=clear_inputs, inputs=None, outputs=[inputs, output_box])
    
        # Submit 및 Clear 버튼을 한 줄에 배치
        with gr.Row():
            submit_button
            clear_button
    
    # 인터페이스 실행
    iface.launch(server_name="127.0.0.1", server_port=7866, inbrowser=True)

    위 코드를 실행하고, http://127.0.0.1:7866/주소를 브라우저 사이드바에서 열면 아래와 같이 초기화면이 열립니다. 

    Ollama Tool 활용 인터넷 검색 및 날씨정보 조회 사이드바 초기화면
    Ollama Tool 활용 인터넷 검색 및 날씨정보 조회 사이드바: 날씨 조회
    Ollama Tool 활용 인터넷 검색 및 날씨정보 조회 사이드바: 시간대별 날씨 조회
    Ollama Tool 활용 인터넷 검색 및 날씨정보 조회 사이드바: 인터넷 검색

    맺음말

    이 블로그를 통해 Ollama의 Tool 기능에 대해 자세히 알아보고, 다양한 도구를 활용하여 더 복잡한 작업을 수행하는 방법을 살펴보았습니다. Ollama는 최신 AI 모델을 통해 외부 세계와의 상호작용을 가능하게 하며, Llama 3.1, Mistral Nemo와 같은 모델을 통해 다양한 기능을 구현할 수 있습니다. 특히, 날씨 정보 조회, 웹 브라우징, 계산기 기능 등을 통해 AI의 활용 범위를 넓히는 데 기여하고 있습니다.

     

    이번 블로그에서는 Gradio를 사용하여 웹 인터페이스를 구축하고, Ollama AI 모델을 통해 날씨 정보와 검색 결과를 제공하는 예제를 구현했습니다. 이를 통해 실시간 날씨 데이터 검색, DuckDuckGo를 통한 인터넷 검색, 그리고 간단한 번역 기능까지 다양한 기능을 실습해 보았습니다.

     

    Ollama Tool 기능의 적용 사례를 통해 여러분의 AI 활용에 도움이 되시기를 바라면서 저는 다음 시간에 더 유익한 정보를 가지고 다시 찾아뵙겠습니다. 감사합니다. 다음 블로그 포스트에서 또 만나요! 🌟

     

    Ollama Tool 예제 제작 및 사용후기 세줄

    • API 방식과 달리 Tool방식은 자연어로 검색할 수 있어서 편리하다.
    • 광역시, 특별시, 시 등 복잡한 도시명의 반환은 AI에게 프롬프트로 요청한다. 
    • Mistral NeMo 모델은 Llama 3.1 보다 한국어 표현이 자연스럽다. 

     

    https://fornewchallenge.tistory.com/

     

     

    2024.07.26 - [AI 언어 모델] - 🧠 최강 AI 검색 비서: Mistral Large 2 모델 설정 가이드

     

    🧠 최강 AI 검색 비서: Mistral Large 2 모델 설정 가이드

    안녕하세요! 오늘은 뛰어난 한국어와 명쾌한 추론 능력으로 관심을 모으로 있는 Mistral Large 2 모델을 브라우저 사이드바에 고정해서 인터넷 검색 비서로 활용하는 방법을 알아보겠습니다. 이전

    fornewchallenge.tistory.com

     

     

    728x90