├── .env ├── .gitignore ├── 01_simple_client.py ├── 02_ollama_client.py ├── 03_context_client.py ├── 04_agent_langchain.py ├── 05_agent_langgraph.py ├── 06_agent_pydantic.py ├── 07_mcp_std_pydantic.py ├── 08_mcp_sse_pydantic.py ├── README.md ├── context.csv ├── dundie_mcp.py └── requirements.txt /.env: -------------------------------------------------------------------------------- 1 | export DUNDIE_PASSWORD=magic 2 | export DUNDIE_EMAIL=schrute@dundlermifflin.com 3 | export OLLAMA_URL=http://192.168.1.153:11434 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | .ropeproject 3 | __pycache__/ 4 | dundie.log 5 | -------------------------------------------------------------------------------- /01_simple_client.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import requests 5 | 6 | OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434") 7 | 8 | 9 | def ask_ollama(prompt): 10 | url = f"{OLLAMA_URL}/api/chat" 11 | headers = {"Content-Type": "application/json", "accept": "application/json"} 12 | model = "qwen3:0.6b" 13 | system_prompt = "Answer in Portuguese" 14 | payload = { 15 | "model": model, 16 | "messages": [ 17 | {"role": "system", "content": system_prompt}, 18 | {"role": "user", "content": prompt}, 19 | ], 20 | } 21 | 22 | response = requests.post(url, headers=headers, json=payload, stream=True) 23 | for line in response.iter_lines(): 24 | if line: 25 | data = json.loads(line.decode("utf-8")) 26 | message = data.get("message", {}).get("content", "") 27 | if message: 28 | print(message, end="") 29 | print() 30 | 31 | 32 | def main(): 33 | while True: 34 | prompt = input("Enter your prompt: ") 35 | if not prompt: 36 | print("Prompt cannot be empty.") 37 | continue 38 | if prompt.strip() in ["exit", "quit", "q"]: 39 | print("Exiting...") 40 | break 41 | ask_ollama(prompt) 42 | print("-" * 50) 43 | 44 | 45 | if __name__ == "__main__": 46 | main() 47 | -------------------------------------------------------------------------------- /02_ollama_client.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import ollama 4 | 5 | OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434") 6 | 7 | 8 | def ask_ollama(prompt): 9 | # Set the Ollama API base URL 10 | client = ollama.Client(host=OLLAMA_URL) 11 | 12 | model = "qwen3:0.6b" 13 | system_prompt = "Answer in Portuguese" 14 | 15 | # Create the messages list 16 | messages = [ 17 | {"role": "system", "content": system_prompt}, 18 | {"role": "user", "content": prompt}, 19 | ] 20 | 21 | # Make the streaming request 22 | stream = client.chat(model=model, messages=messages, stream=True) 23 | 24 | # Process and print the streamed response 25 | for chunk in stream: 26 | message_chunk = chunk.get("message", {}).get("content", "") 27 | if message_chunk: 28 | print(message_chunk, end="") 29 | print() 30 | 31 | 32 | def main(): 33 | while True: 34 | prompt = input("Enter your prompt: ") 35 | if not prompt: 36 | print("Prompt cannot be empty.") 37 | continue 38 | if prompt.strip() in ["exit", "quit", "q"]: 39 | print("Exiting...") 40 | break 41 | ask_ollama(prompt) 42 | print("-" * 50) 43 | 44 | 45 | if __name__ == "__main__": 46 | main() 47 | -------------------------------------------------------------------------------- /03_context_client.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import ollama 4 | 5 | OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434") 6 | CONTEXT = open("context.csv").read() 7 | 8 | 9 | def ask_ollama(prompt): 10 | # Set the Ollama API base URL 11 | client = ollama.Client(host=OLLAMA_URL) 12 | 13 | model = "llama3.1" 14 | system_prompt = "Analize the assistant content and give short answers" 15 | 16 | # Create the messages list 17 | messages = [ 18 | {"role": "system", "content": system_prompt}, 19 | { 20 | "role": "assistant", 21 | "content": f"Here is the list of feature, version and state for all the Magic System features.\n {CONTEXT}", 22 | }, 23 | {"role": "user", "content": prompt}, 24 | ] 25 | 26 | # Make the streaming request 27 | stream = client.chat(model=model, messages=messages, stream=True) 28 | 29 | # Process and print the streamed response 30 | for chunk in stream: 31 | message_chunk = chunk.get("message", {}).get("content", "") 32 | if message_chunk: 33 | print(message_chunk, end="") 34 | print() 35 | 36 | 37 | def main(): 38 | while True: 39 | prompt = input("Enter your prompt: ") 40 | if not prompt: 41 | print("Prompt cannot be empty.") 42 | continue 43 | if prompt.strip() in ["exit", "quit", "q"]: 44 | print("Exiting...") 45 | break 46 | ask_ollama(prompt) 47 | print("-" * 50) 48 | 49 | 50 | if __name__ == "__main__": 51 | main() 52 | -------------------------------------------------------------------------------- /04_agent_langchain.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import subprocess 4 | 5 | from langchain.agents import initialize_agent 6 | from langchain.tools import Tool 7 | from langchain_ollama import OllamaLLM 8 | 9 | OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434") 10 | model = os.environ.get("MODEL", "gemma3:12b") 11 | llm = OllamaLLM(model=model, base_url=OLLAMA_URL) 12 | 13 | 14 | def list_transactions(email: str) -> list[dict]: 15 | """List user transaction by email""" 16 | try: 17 | # Executa o comando da CLI 18 | result = subprocess.run( 19 | ["dundie", "list", "--email", email, "--asjson"], 20 | capture_output=True, 21 | text=True, 22 | check=True, 23 | ) 24 | return json.loads(result.stdout.strip()) 25 | except subprocess.CalledProcessError as e: 26 | print(f"Error listing transactions: {e}") 27 | return [] 28 | 29 | 30 | tools = [ 31 | Tool( 32 | name="list_transactions", 33 | func=list_transactions, 34 | description="Use this when you need to list user transactions.", 35 | ) 36 | ] 37 | 38 | agent = initialize_agent( 39 | llm=llm, tools=tools, verbose=True, agent_type="zero-shot-react-description" 40 | ) 41 | 42 | 43 | def invoke_agent(prompt: str) -> dict: 44 | return agent.invoke({"input": prompt}) 45 | 46 | 47 | def main(): 48 | while True: 49 | prompt = input("Enter your prompt: ") 50 | if not prompt: 51 | print("Prompt cannot be empty.") 52 | continue 53 | if prompt.strip() in ["exit", "quit", "q"]: 54 | print("Exiting...") 55 | break 56 | result = invoke_agent(prompt) 57 | print("-" * 50) 58 | print(result) 59 | 60 | 61 | if __name__ == "__main__": 62 | main() 63 | -------------------------------------------------------------------------------- /05_agent_langgraph.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import subprocess 4 | 5 | from langchain_core.tools import tool 6 | from langchain_ollama import ChatOllama 7 | from langgraph.prebuilt import create_react_agent 8 | 9 | OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434") 10 | model = os.environ.get("MODEL", "qwen3:4b") 11 | llm = ChatOllama(model=model, base_url=OLLAMA_URL) 12 | 13 | 14 | @tool 15 | def list_transactions(email: str) -> list[dict]: 16 | """List user transaction by email""" 17 | try: 18 | # Executa o comando da CLI 19 | result = subprocess.run( 20 | ["dundie", "list", "--email", email, "--asjson"], 21 | capture_output=True, 22 | text=True, 23 | check=True, 24 | ) 25 | return json.loads(result.stdout.strip()) 26 | except subprocess.CalledProcessError as e: 27 | print(f"Error listing transactions: {e}") 28 | return [] 29 | 30 | 31 | @tool 32 | def sum_transactions(transactions: list[dict]) -> float: 33 | """ 34 | Sum the values of a list of transactions. 35 | """ 36 | print("sum_transactions", len(transactions)) 37 | return sum([t["value"] for t in transactions]) 38 | 39 | 40 | # Create the agent 41 | agent = create_react_agent( 42 | llm, 43 | tools=[list_transactions, sum_transactions], 44 | ) 45 | 46 | 47 | def invoke_agent(prompt: str) -> dict: 48 | return agent.invoke({"messages": [{"role": "user", "content": prompt}]}) 49 | 50 | 51 | def main(): 52 | while True: 53 | prompt = input("Enter your prompt: ") 54 | if not prompt: 55 | print("Prompt cannot be empty.") 56 | continue 57 | if prompt.strip() in ["exit", "quit", "q"]: 58 | print("Exiting...") 59 | break 60 | result = invoke_agent(prompt) 61 | print("-" * 50) 62 | print(result) 63 | 64 | 65 | if __name__ == "__main__": 66 | main() 67 | -------------------------------------------------------------------------------- /06_agent_pydantic.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import subprocess 4 | from typing import Any, Dict, List 5 | 6 | from pydantic_ai import Agent 7 | from pydantic_ai.agent import AgentRunResult 8 | from pydantic_ai.models.openai import OpenAIModel 9 | from pydantic_ai.providers.openai import OpenAIProvider 10 | 11 | # Set up Ollama LLM 12 | OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434") 13 | model = os.environ.get("MODEL", "qwen3:4b") 14 | model = OpenAIModel( 15 | model_name=model, provider=OpenAIProvider(base_url=f"{OLLAMA_URL}/v1") 16 | ) 17 | 18 | 19 | def list_transactions(email: str) -> list[dict]: 20 | """List user transaction by email""" 21 | try: 22 | result = subprocess.run( 23 | ["dundie", "list", "--email", email, "--asjson"], 24 | capture_output=True, 25 | text=True, 26 | check=True, 27 | ) 28 | return json.loads(result.stdout.strip()) 29 | except subprocess.CalledProcessError as e: 30 | print(f"Error listing transactions: {e}") 31 | return [] 32 | 33 | 34 | def sum_transactions(transactions: List[Dict[str, Any]]) -> float: 35 | """ 36 | Sum the amounts of a list of transactions. 37 | """ 38 | return sum(t["value"] for t in transactions) 39 | 40 | 41 | agent = Agent( 42 | model, 43 | tools=[list_transactions, sum_transactions], 44 | ) 45 | 46 | 47 | def invoke_agent(prompt: str) -> AgentRunResult: 48 | return agent.run_sync(prompt) 49 | 50 | 51 | def main(): 52 | while True: 53 | prompt = input("Enter your prompt: ") 54 | if not prompt: 55 | print("Prompt cannot be empty.") 56 | continue 57 | if prompt.strip() in ["exit", "quit", "q"]: 58 | print("Exiting...") 59 | break 60 | result = invoke_agent(prompt) 61 | print("-" * 50) 62 | print(result) 63 | 64 | 65 | if __name__ == "__main__": 66 | main() 67 | -------------------------------------------------------------------------------- /07_mcp_std_pydantic.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | from typing import Any, Dict, List 4 | 5 | from pydantic_ai import Agent 6 | from pydantic_ai.agent import AgentRunResult 7 | from pydantic_ai.mcp import MCPServerStdio 8 | from pydantic_ai.models.openai import OpenAIModel 9 | from pydantic_ai.providers.openai import OpenAIProvider 10 | 11 | # Set up Ollama LLM 12 | OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434") 13 | model = os.environ.get("MODEL", "qwen3:4b") 14 | model = OpenAIModel( 15 | model_name=model, provider=OpenAIProvider(base_url=f"{OLLAMA_URL}/v1") 16 | ) 17 | 18 | dundie_mcp = MCPServerStdio( 19 | "fastmcp", 20 | args=["run", "dundie_mcp.py"], 21 | ) 22 | 23 | 24 | def sum_transactions(transactions: List[Dict[str, Any]]) -> float: 25 | """ 26 | Sum the amounts of a list of transactions. 27 | """ 28 | return sum(t["value"] for t in transactions) 29 | 30 | 31 | agent = Agent(model, tools=[sum_transactions], mcp_servers=[dundie_mcp]) 32 | 33 | 34 | async def invoke_agent(prompt: str) -> AgentRunResult: 35 | return await agent.run(prompt) 36 | 37 | 38 | async def main(): 39 | async with agent.run_mcp_servers(): 40 | while True: 41 | prompt = input("Enter your prompt: ") 42 | if not prompt: 43 | print("Prompt cannot be empty.") 44 | continue 45 | if prompt.strip() in ["exit", "quit", "q"]: 46 | print("Exiting...") 47 | break 48 | result = await invoke_agent(prompt) 49 | print(result) 50 | 51 | 52 | if __name__ == "__main__": 53 | asyncio.run(main()) 54 | -------------------------------------------------------------------------------- /08_mcp_sse_pydantic.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | from typing import Any, Dict, List 4 | 5 | from pydantic_ai import Agent 6 | from pydantic_ai.agent import AgentRunResult 7 | from pydantic_ai.mcp import MCPServerHTTP 8 | from pydantic_ai.models.openai import OpenAIModel 9 | from pydantic_ai.providers.openai import OpenAIProvider 10 | 11 | # Set up Ollama LLM 12 | OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434") 13 | model = os.environ.get("MODEL", "qwen3:4b") 14 | model = OpenAIModel( 15 | model_name=model, provider=OpenAIProvider(base_url=f"{OLLAMA_URL}/v1") 16 | ) 17 | 18 | MCP_URL = os.environ.get("MCP_URL", "http://localhost:8000/sse") 19 | dundie_mcp = MCPServerHTTP(MCP_URL) 20 | 21 | 22 | def sum_transactions(transactions: List[Dict[str, Any]]) -> float: 23 | """ 24 | Sum the amounts of a list of transactions. 25 | """ 26 | return sum(t["value"] for t in transactions) 27 | 28 | 29 | agent = Agent(model, tools=[sum_transactions], mcp_servers=[dundie_mcp]) 30 | 31 | 32 | async def invoke_agent(prompt: str) -> AgentRunResult: 33 | return await agent.run(prompt) 34 | 35 | 36 | async def main(): 37 | async with agent.run_mcp_servers(): 38 | while True: 39 | prompt = input("Enter your prompt: ") 40 | if not prompt: 41 | print("Prompt cannot be empty.") 42 | continue 43 | if prompt.strip() in ["exit", "quit", "q"]: 44 | print("Exiting...") 45 | break 46 | result = await invoke_agent(prompt) 47 | print(result) 48 | 49 | 50 | if __name__ == "__main__": 51 | asyncio.run(main()) 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Experimentos com Ollama 2 | 3 | ## Instalando Ollama 4 | 5 | Acesse https://ollama.com/download para baixar e instalar o Ollama. 6 | 7 | Se estiver no Linux pode usar o seguinte comando: 8 | 9 | ```bash 10 | curl -fsSL https://ollama.com/install.sh | sh 11 | ``` 12 | 13 | Uma vez instalado certifique de que o servidor ollama está rodando: 14 | 15 | ```bash 16 | ollama serve 17 | ``` 18 | Mantenha este terminal aberto para que o servidor continue rodando. 19 | Se a mensagem informar que já está rodando, pode ignorar este passo. 20 | 21 | ## Modelos 22 | 23 | Para instalar novos modelos: 24 | 25 | ```bash 26 | ollama install 27 | ``` 28 | 29 | Verifique os modelos disponíveis no link https://ollama.com/models 30 | 31 | Usaremos os seguintes modelos: 32 | 33 | > [!INFO] 34 | > Verifique o quanto possui de espaço livre em disco e memória RAM antes de instalar os modelos, 35 | > Todos esses modelos ocupam pelo menos 16GB de espaço em disco e requerem entre 2 e 12 GB de memória RAM para executar. 36 | 37 | ```bash 38 | ollama pull qwen3:0.6b 39 | ollama pull qwen3:4b 40 | ollama pull llama3.1 41 | ollama pull gemma3:12b 42 | ``` 43 | 44 | Para listar os modelos disponíveis: 45 | 46 | ```bash 47 | ollama ls 48 | ``` 49 | 50 | ## Clonando o repositório 51 | 52 | Clone este repositório: 53 | 54 | ```bash 55 | git clone https://github.com/rochacbruno/python-base-ai 56 | cd python-base-ai 57 | ``` 58 | 59 | ## Introdução 60 | 61 | Vamos abordar a interação com LLM (Large Language Models), que podem ser serviços contratados como OpenAI/GPT, Claude, ou modelos locais como Ollama que acabamos de instalar. 62 | 63 | O nosso objetivo é experimentar as diversas formar de interagir com esses modelos através do Python e de conceitos padronizados como CLients, Agentes e MCP. 64 | 65 | Não iremos abordar nenhum tópico relacionado a treinamento de modelos, RAG, e nem discutir profundamente a qualidade ou questões éticas de cada modelo. 66 | 67 | Nosso foco aqui é extritamente em como, no papel de pessoa que desenvolve programas com Python, você pode integrar com esses modelos. 68 | 69 | ### Componentes 70 | 71 | - LLM: Modelos de AI Generativa que são treinados a partir de dados de diversas fontes, cada modelo tem uma quantidade especifica de parametros, quanto mais parametros, mais complexo e preciso é o modelo, mas mais custoso e demorado é o tempo de resposta. Uma conta básica e estimada que podemos fazer é da proporção de 1GB de RAM para cada bilhão de parametros do modelo, e além da RAM, o modelo também vai precisar de processamento, preferencialmente GPU, porém é possivel sim usar apenas CPU, sendo que nesse caso o processamento será mais lento. 72 | - Providers: Provedores de modelos são serviços que expoem uma interface (API ou UI) para acessarmos o modelo, podemos executar provedores localmente, como é o caso do Ollama, ou pagar para obter uma chave de API e usar provedores externos como OpenAI ou Antropic, esses serviços geralmente cobram pela quantidade de tokens processados. 73 | - Prompt: Uma sequência de texto que é enviada para o modelo para que ele gere uma resposta. 74 | - Token: Um token é uma unidade de medida usada para medir a quantidade de texto que um modelo pode processar. 75 | - Agente: Um agente é uma entidade que interage com um modelo de AI Generativa, ele pode ser um humano ou um programa, e é responsável por enviar prompts para o modelo e receber respostas. 76 | - Tools: Tools (ferramentas) são funções que são registradas através do agente e que dependendo do modelo podem ser invocadas para obter contexto, nem todos os modelos suportam tools. 77 | - MCP: Model Context Protocol é um protocolo para criar APIs que interagem com diversos sistemas e podem prover contexto adicional aos agentes e operar como **tools** para os modelos. 78 | 79 | ## Clientes 80 | 81 | O provedor de LLM expoe uma API Rest com suporte a streaming (SSE ou Streamable-HTTP), essas APIs geralmente são padronizadas e fornecem informações sobre os modelos, 82 | e algumas maneiras de interação como APIS de **generate**, **chat** e **completion**. 83 | 84 | No Ollama temos algumas APIs interessantes: 85 | 86 | Listar os models: 87 | 88 | ```bash 89 | curl -X GET http://localhost:11434/api/tags 90 | {"models":[{"name":"llama3.1:latest","model":"llama3.1:latest"}, {"name":"llama2:latest","model":"llama2:latest"}]} 91 | ``` 92 | 93 | Generate é uma API usada para gerar texto a partir de um prompt. 94 | 95 | ``` 96 | curl -X POST http://localhost:11434/api/generate -H "Content-Type: application/json" -d '{"prompt":"Hello"}' 97 | {"id":"1234567890","model":"llama3.1:latest","prompt":"Hello","completion":"Hello, how are you?"} 98 | ``` 99 | 100 | Chat é uma API usada para interagir com um modelo de AI Generativa em um contexto de conversa, a diferença é que 101 | esta API mantém contexto entre as chamadas. 102 | 103 | ``` 104 | curl -X POST http://localhost:11434/api/chat -H "Content-Type: application/json" -d '{"prompt":"Hello"}' 105 | {"id":"1234567890","model":"llama3.1:latest","prompt":"Hello","completion":"Hello, how are you?"} 106 | ``` 107 | 108 | Para mais endpoints, visite a documentação oficial do Ollama. 109 | 110 | ### Interagindo com Ollama pela linha de comando 111 | 112 | ### Usando CURL 113 | 114 | ```bash 115 | curl -X POST http://localhost:11434/api/generate -H "Content-Type: application/json" -d '{"prompt":"Hello", "model": "qwen3:0.6b", "stream": false}' 116 | 117 | { 118 | "model":"qwen3:0.6b", 119 | "created_at":"2025-05-23T18:25:46.508616186Z", 120 | "response":"\u003cthink\u003e\nOkay, the user said \"Hello\" and I need to respond. Let me start by acknowledging their greeting. A simple \"Hello!\" is good.\n\u003c/think\u003e\n\n 121 | Hello! How can I assist you today? 😊 Have a great day!", 122 | "done":true, 123 | "done_reason":"stop", 124 | "context":[], 125 | "total_duration":2556592493, 126 | "load_duration":25473441, 127 | "prompt_eval_count":9, 128 | "prompt_eval_duration":22163075, 129 | "eval_count":112, 130 | "eval_duration":2508279738 131 | } 132 | ``` 133 | 134 | ### Ambiente Python 135 | 136 | Para começar crie um ambiente virtual na raiz do repositório. 137 | 138 | 139 | ```bash 140 | python3 -m venv .venv 141 | ``` 142 | 143 | Ative o ambiente 144 | 145 | ```bash 146 | source .venv/bin/activate 147 | ``` 148 | 149 | Instale os requisitos 150 | 151 | ```bash 152 | pip install -r requirements.txt 153 | ``` 154 | 155 | ### Cliente simples usando Requests 156 | 157 | Da mesma forma que usamos CURL para interagir com o modelo, podemos usar a biblioteca Requests para fazer chamadas HTTP. 158 | 159 | O funcionamento é bastante similar, o código abaixo é auto-explicativo leia linha a linha: 160 | 161 | ```py 162 | def ask_ollama(prompt: str): 163 | url = f"{OLLAMA_URL}/api/chat" 164 | headers = { 165 | "Content-Type": "application/json", 166 | "accept": "application/json" 167 | } 168 | model = "qwen3:0.6b" 169 | system_prompt = "Answer in Portuguese" # Aqui dá para ser criativo e personalizar o comportamento do modelo 170 | payload = { 171 | "model": model, 172 | "messages": [ 173 | {"role": "system", "content": system_prompt}, 174 | {"role": "user", "content": prompt} 175 | ] 176 | } 177 | 178 | response = requests.post(url, headers=headers, json=payload, stream=True) 179 | 180 | # A resposta é um gerador de chunks pois o Ollama retorna com SSE por default 181 | # portanto precisamos iterar sobre os chunks e processá-los 182 | for line in response.iter_lines(): 183 | if line: 184 | data = json.loads(line.decode("utf-8")) 185 | message = data.get("message", {}).get("content", "") 186 | if message: 187 | print(message, end="") 188 | ``` 189 | 190 | O programa completo está no exemplo `01_simple_client.py` 191 | 192 | Execute: 193 | 194 | > [!INFO] 195 | > Este exemplo usa o modelo qwen3:0.6b por ser o mais leve, porém não é o mais preciso, pode alterar o modelo caso tenha recursos de processamento. 196 | 197 | ```bash 198 | python3 01_simple_client.py 199 | ``` 200 | Exemplo: 201 | 202 | ```console 203 | ❯ python 01_simple_client.py 204 | Enter your prompt: What is a Barometer? 205 | 206 | Okay, the user is asking what a Barometer is. First, I need to explain the basic concept. A Barometer is a device used to measure atmospheric pressure. I remember from school that it's a simple device with a mercury column. But I should clarify that Mercury is a liquid, right? And it's used to measure the pressure. 207 | 208 | Wait, maybe I should mention the units. Oh, right, it's based on the height of mercury column. So when the pressure decreases, the mercury falls, and vice versa. Also, note that the unit is inches of mercury, so the user might be familiar with that. 209 | 210 | I should also mention that the Barometer is a basic tool, so it's used in various applications. Maybe include some examples, like in weather forecasting or as a tool for altitude measurement. Oh, and maybe mention that it's not a scale but a gauge. That adds depth to the explanation. Let me check if I'm missing anything important. No, I think that's a solid answer. 211 | 212 | 213 | Um **Barômetro** é um dispositivo que mede a **pressão atmosférica**. Ele funciona com um **coluna de mercurio** que indica a força atmosférica em pés de mercurio. A pressão atmosférica é medida pela altura dessa coluna, e quando a pressão diminui, a coluna do mercurio se eleva. 214 | ``` 215 | 216 | > [!TIP] 217 | > Este modelo o qwen3:0.6b é um reasoning model, ou seja, ele pensa e analiza o próprio pensamento antes de responde, podemos omitir o texto entre `` colocando o comando `/no_think` no system prompt. 218 | 219 | ### Cliente simples usando a lib ollama 220 | 221 | O código que usamos no exemplo anterior usando **requests** pode ser padronizado pois existe um wrapper para o ollama que torna o uso mais ergonômico e fácil de usar. 222 | 223 | ```py 224 | import ollama 225 | 226 | def ask_ollama(prompt): 227 | client = ollama.Client(host=OLLAMA_URL) 228 | model = "qwen3:0.6b" 229 | system_prompt = "Answer in Portuguese" 230 | messages = [ 231 | {"role": "system", "content": system_prompt}, 232 | {"role": "user", "content": prompt} 233 | ] 234 | stream = client.chat( 235 | model=model, 236 | messages=messages, 237 | stream=True 238 | ) 239 | for chunk in stream: 240 | message_chunk = chunk.get('message', {}).get('content', '') 241 | if message_chunk: 242 | print(message_chunk, end="") 243 | print() 244 | ``` 245 | 246 | O código completo está no exemplo `02_ollama_client.py` 247 | 248 | ```console 249 | ❯ python 02_ollama_client.py 250 | Enter your prompt: Qual é a raiz quadrada de 81? 251 | 252 | Okay, the user is asking for the square root of 81. Let me think. I know that when you square a number, you get the original number. So if I square 9, it's 81. That makes sense. But wait, are there other numbers that when squared give 81? Let me check. 10 squared is 100, so no, that's too big. What about negative numbers? Well, a negative number squared is positive, so the square root should be positive. So the answer should be 9. Let me confirm that with another method. If I divide 81 by 9, that's 9. So that's consistent. I think that's it. 253 | 254 | 255 | A raíz quadrada de 81 é $ \sqrt{81} = 9 $. 256 | ``` 257 | 258 | ## Contexto 259 | 260 | Até aqui usamos o modelo apenas com os prompts (system e user), a resposta do modelo será baseada na análise dos dados usados em seu treinamento, que geralmente 261 | é antigo, ou pelo menos com alguns meses de defasagem, pois demora bastante tempo para treinar um modelo com bilhões de parâmetros. 262 | 263 | Portanto para respostas mais atualizadas precisamos fornecer **contexto extra**, e existem várias abordagens para fornecer contexto, 264 | a mais simples é fornecer o contexto no próprio prompt, é bastante usada em interações com chatbots, passando pelo uso de **assistant** prompts 265 | que é a mesma coisa porém melhor estruturada e transparente para o usuário do chatbot, e as técnicas mais avançadas envolvem os conceitos de 266 | **tools**, **MCP** e **RAG**. 267 | 268 | Vamos abordar os 3 primeiros 269 | - **injeção de contexto no prompt** - que possui limitações principalmente no tamanho dos tokens 270 | - **tools** - Que é uma padronização para disponibilizar funções que o modelo invoca através do agente 271 | - **MCP** - QUe é uma padronização para criação de APIs que fornecem contexto 272 | 273 | Não abordaremos **RAG** pois esta estratégia exige o uso de bandos de dados de vetor bastante especializados, que além de demandar recursos também demoram para serem alimentados. 274 | 275 | 276 | ### Cliente com injeção de contexto 277 | 278 | O contexto que usaremos é texto, e o que é mais importante para avaliar se vale a pena usar esta estratégia é verificar a quantidade de tokens do nosso contexto injetado, 279 | em modelos locais isso pode significar maior uso de recursos, mais tempo de processamento e em provedores externos geralmente a cobrança é feita pelo número de tokens, portanto 280 | pagaremos mais se injetarmos muito contexto dessa forma. 281 | 282 | Neste exemplo imagine que queremos criar um **Chatbot** que vai analisar um software que estamos lançando, usaremos para fazer perguntas e gerar textos de divulgação, 283 | o nosso contexto é o seguinte: 284 | 285 | ```md 286 | # Features Included in Each of our software versions 287 | 288 | Here is the list of features included in the most 289 | recent versions of our Magic Software. 290 | 291 | Feature,Version,State 292 | User Authentication,1.0.2,stable 293 | Data Encryption,2.0.0,stable 294 | Cloud Synchronization,1.1.0,unstable 295 | Offline Mode,1.0.2,stable 296 | 297 | ``` 298 | 299 | Esse conteúdo está salvo no arquivo `context.csv` e suponha que é uma informação estática, documentação, ou que usamos alguma rotina externa para manter atualizada. 300 | 301 | Alguns pontos importantes: 302 | 303 | - Quanto menos tokens melhor, portanto é melhor um CSV do que um JSON 304 | - Use https://huggingface.co/spaces/Xenova/the-tokenizer-playground para calcular 305 | - É preciso contextualizar, portanto tem que ter headers e no prompt final incluir text instrutivo 306 | 307 | Como vamos passar esse contexto para o LLM? 308 | 309 | ```py 310 | CONTEXT = open("context.csv").read() 311 | 312 | def ask_ollama(prompt): 313 | client = ollama.Client(host=OLLAMA_URL) 314 | model = "llama3.1" 315 | system_prompt = "Analize the assistant content and give short answers" 316 | 317 | messages = [ 318 | {"role": "system", "content": system_prompt}, 319 | # Context Injection: 320 | {"role": "assistant", "content": f"Here is the list of feature, version and state for all the Magic System features.\n {CONTEXT}"}, 321 | {"role": "user", "content": prompt}, 322 | ] 323 | 324 | stream = client.chat( 325 | model=model, 326 | messages=messages, 327 | stream=True 328 | ) 329 | ``` 330 | 331 | > [!INFO] 332 | > Para este exemplo precisamos de um modelo melhor, o qwen3:0.6b não dá conta de analizar o contexto, portanto usaremos os llama3.1 que tem 8bi de parâmetros, 333 | > isso significa que a reposta será mais demorada e vai consumir pelo menos 8GB de RAM. 334 | 335 | Agora podemos fazer perguntas mais elaboradas e contando com o contexto injetado: 336 | 337 | 338 | ```console 339 | ❯ python 03_context_client.py 340 | Enter your prompt: Which versions did we released? 341 | We released: 342 | 343 | 1. Version 1.0.2 (with features: User Authentication, Data Encryption, Offline Mode, Push Notifications) 344 | 2. Version 1.1.0 (with features: Cloud Synchronization, Multi-factor Authentication) 345 | 3. Version 2.0.0 (with features: Dark Theme, Realtime Collaboration, API Integration) 346 | -------------------------------------------------- 347 | Enter your prompt: 348 | ``` 349 | 350 | ```console 351 | ❯ python 03_context_client.py 352 | Enter your prompt: Create a promotional marketing message covering the stable features of version 2.0.0 353 | Here's a promotional marketing message: 354 | 355 | **Introducing Magic System 2.0.0: Unlock Unparalleled Security and Collaboration!** 356 | 357 | Take your productivity to new heights with our latest update, featuring a robust suite of stable features designed to revolutionize the way you work. 358 | 359 | **Unmatched Security** 360 | 361 | * **Data Encryption**: Protect your sensitive information with top-notch encryption methods. 362 | * **Multi-factor Authentication**: Add an extra layer of security to prevent unauthorized access. 363 | * **Role-based Access Control**: Limit user permissions to ensure data integrity and accountability. 364 | 365 | **Collaborate Seamlessly** 366 | 367 | * **Dark Theme**: Improve focus and reduce eye strain with our sleek dark mode. 368 | * **API Integration**: Integrate Magic System with your favorite apps for effortless workflow management. 369 | 370 | Experience the power of stability and innovation with Magic System 2.0.0. Upgrade now and discover a more secure, efficient, and collaborative work environment! 371 | ``` 372 | 373 | > [!INFO] 374 | > Quanto mais otimizado o modelo, melhor será a reposta. 375 | 376 | ## Agentes 377 | 378 | Até aqui falamos de clients, que são formas passivas de interagir com LLMs, 379 | mandamos um prompt e a AI responde. 380 | 381 | Agentes são uma outra categoria de clients, sim, eles ainda são clientes, mas possuem 382 | capacidades extra, pense que um **agente** de AI é como **você**, a idéia é justamente essa, todas as tarefas que **você** geralmente teria que manualmente executar para preparar o **contexto**, otimizar o **prompt** ou as ações que você faria com o resultado da resposta da AI são coisas que geralmente são automatizadas com agentes. 383 | 384 | O Ecossistema de agentes ainda está bastante imaturo (no momento da escrita deste material), alguns protocolos e padrões já estão sendo estabelecidos, porém ainda tem muita coisa surgindo e mudando, portanto esta é uma area que pode ser completamente diferente em poucos meses. 385 | 386 | Aqui pretendo abordar o cenário atual, dentro das pesquisas que fui capaz de fazer. 387 | 388 | Abordaremos agentes **simples** que terão dois objetivos: 389 | 390 | 1. Fornecer contexto dinamicamente 391 | 2. Executar ações a partir da resposta da AI 392 | 393 | Esses agentes que criaremos irão funcionar a partir da invocação de um prompt humano, ou seja, teremos que pedir. 394 | 395 | Existe uma categoria de agentes mais evoluida, o que chamamos de **agentic**, esse é um modelo onde criamos agentes que interagem com outros agentes, para desenvolver este tipo de agentes precisamos criar uma estrutura mais complexa envolvendo um **grafo** de agentes, e usar modelos com suporte mais poderoso a contextualização. 396 | 397 | ### O que é um agente? 398 | 399 | Um Programa que faz interações com a LLM de forma automatizada, a partir de uma instrução que é dada através de um prompt, ou seja, uma versão mais moderna e especializada dos clientes que acabamos de criar. 400 | 401 | ### Tools 402 | 403 | O protocolo de **tools** é um dos principais motivos para usarmos agentes, 404 | ele permite que registremos funções que serão invocadas pelo agente, sempre que o modelo 405 | necessitar de contexto adicional ou quiser efetuar uma ação. 406 | 407 | Não são todos os modelos que suportam o uso de **tools**, no caso do Ollama podemos 408 | ver a lista de modelos, e um outro detalhe é que para conseguir decidir o uso de tools 409 | o modelo precisa ter mais parametros, nos meus testes, apenas modelos com 8b ou mais conseguem usar tools de forma eficiente. 410 | 411 | Modelos com suporte a tools. 412 | https://ollama.com/search?c=tools 413 | 414 | 415 | ### Tools para fazer o que? 416 | 417 | Lembra que criamos um programa chamado `dundie` que controlar uma conta corrente de pontos de funcionários? 418 | 419 | E se criarmos um agente de AI para consultarmos os pontos, e talvez um para adicionar pontos? 420 | 421 | ### Agente com Langchain 422 | 423 | > [!INFO] 424 | > Para conseguir selecionar tools com langchain o modelo tem que ser pelo menos de 12b com suporte a tools, 425 | > testei todos os outros como llama3.1:8b e nenhum foi capaz de encontrar as tools. 426 | 427 | ```py 428 | model = os.environ.get("MODEL", "gemma3:12b") 429 | llm = OllamaLLM(model=model, base_url=OLLAMA_URL) 430 | 431 | def list_transactions(email: str) -> list[dict]: 432 | """List user transaction by email""" 433 | try: 434 | result = subprocess.run( 435 | ["dundie", "list", "--email", email, "--asjson"], 436 | capture_output=True, 437 | text=True, 438 | check=True 439 | ) 440 | return json.loads(result.stdout.strip()) 441 | except subprocess.CalledProcessError as e: 442 | print(f"Error listing transactions: {e}") 443 | return [] 444 | 445 | tools = [ 446 | Tool( 447 | name="list_transactions", 448 | func=list_transactions, 449 | description="Use this when you need to list user transactions." 450 | ) 451 | ] 452 | 453 | agent = initialize_agent( 454 | llm=llm, 455 | tools=tools, 456 | verbose=True, 457 | agent_type="zero-shot-react-description" 458 | ) 459 | ``` 460 | 461 | 462 | 463 | ```console 464 | ❯ python 04_agent_langchain.py 465 | Enter your prompt: How many transactions for pam@dm.com? 466 | 467 | 468 | > Entering new AgentExecutor chain... 469 | I need to list the transactions for pam@dm.com to determine the number of transactions. 470 | Action: list_transactions 471 | Action Input: pam@dm.com 472 | Observation: [{'date': '23/05/2025 18:55:17', 'actor': 'schrute@dundlermifflin.com', 'value': 1.0, 'email': 'pam@dm.com', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}, {'date': '23/05/2025 12:49:11', 'actor': 'schrute@dundlermifflin.com', 'value': 48.0, 'email': 'pam@dm.com', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}, {'date': '23/05/2025 12:40:34', 'actor': 'schrute@dundlermifflin.com', 'value': 48.0, 'email': 'pam@dm.com', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}, {'date': '20/05/2025 20:21:12', 'actor': 'schrute@dundlermifflin.com', 'value': 59.0, 'email': 'pam@dm.com', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}, {'date': '20/05/2025 20:21:07', 'actor': 'schrute@dundlermifflin.com', 'value': 593.0, 'email': 'pam@dm.com', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}, {'date': '26/10/2024 15:41:57', 'actor': 'system', 'value': 500.0, 'email': 'pam@dm.com', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}] 473 | Thought:I have listed the transactions for pam@dm.com and there are 6 transactions. 474 | Final Answer: 6 475 | 476 | > Finished chain. 477 | -------------------------------------------------- 478 | {'input': 'How many transactions for pam@dm.com?', 'output': '6'} 479 | 480 | ``` 481 | 482 | ```console 483 | ❯ python 04_agent_langchain.py 484 | Enter your prompt: What is the total sum of values that pam@dm.com received? 485 | 486 | 487 | > Entering new AgentExecutor chain... 488 | Okay, I need to find the transactions associated with the email address "pam@dm.com" and then sum the values received in those transactions. To do this, I need to use the `list_transactions` tool. 489 | Action: list_transactions 490 | Action Input: pam@dm.com 491 | Observation: [{'date': '23/05/2025 18:55:17', 'actor': 'schrute@dundlermifflin.com', 'value': 1.0, 'email': 'pam@dm.com', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}, {'date': '23/05/2025 12:49:11', 'actor': 'schrute@dundlermifflin.com', 'value': 48.0, 'email': 'pam@dm.com', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}, {'date': '23/05/2025 12:40:34', 'actor': 'schrute@dundlermifflin.com', 'value': 48.0, 'email': 'pam@dm.com', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}, {'date': '20/05/2025 20:21:12', 'actor': 'schrute@dundlermifflin.com', 'value': 59.0, 'email': 'pam@dm.com', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}, {'date': '20/05/2025 20:21:07', 'actor': 'schrute@dundlermifflin.com', 'value': 593.0, 'email': 'pam@dm.com', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}, {'date': '26/10/2024 15:41:57', 'actor': 'system', 'value': 500.0, 'email': 'pam@dm.com', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}] 492 | Thought:I have the list of transactions for pam@dm.com. Now I need to sum the 'value' field from each transaction. The values are 1.0, 48.0, 48.0, 59.0, 593.0, and 500.0. 493 | 1.0 + 48.0 + 48.0 + 59.0 + 593.0 + 500.0 = 1249.0 494 | 495 | Thought: I now know the final answer 496 | Final Answer: 1249.0 497 | 498 | > Finished chain. 499 | -------------------------------------------------- 500 | {'input': 'What is the total sum of values that pam@dm.com received?', 'output': '1249.0'} 501 | ``` 502 | 503 | ### Agente com LangGraph 504 | 505 | Tudo parecido, porém aparentemente o LangGraph é melhor em contextualizar o modelo, portanto podemos até usar um modelo menor. 506 | 507 | As tools sao registradas via decorator. 508 | 509 | ```py 510 | from langchain_ollama import ChatOllama 511 | from langchain_core.tools import tool 512 | from langgraph.prebuilt import create_react_agent 513 | 514 | 515 | OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434") 516 | model = os.environ.get("MODEL", "qwen3:4b") 517 | llm = ChatOllama(model=model, base_url=OLLAMA_URL) 518 | 519 | @tool 520 | def list_transactions(email: str) -> list[dict]: 521 | """List user transaction by email""" 522 | try: 523 | # Executa o comando da CLI 524 | result = subprocess.run( 525 | ["dundie", "list", "--email", email, "--asjson"], 526 | capture_output=True, 527 | text=True, 528 | check=True 529 | ) 530 | return json.loads(result.stdout.strip()) 531 | except subprocess.CalledProcessError as e: 532 | print(f"Error listing transactions: {e}") 533 | return [] 534 | 535 | @tool 536 | def sum_transactions(transactions: list[dict]) -> float: 537 | """ 538 | Sum the values of a list of transactions. 539 | """ 540 | print("sum_transactions", len(transactions)) 541 | return sum([t["value"] for t in transactions]) 542 | 543 | # Create the agent 544 | agent = create_react_agent( 545 | llm, 546 | tools=[list_transactions, sum_transactions], 547 | ) 548 | 549 | def invoke_agent(prompt: str) -> dict: 550 | return agent.invoke( 551 | { 552 | "messages": [ 553 | {"role": "user", 554 | "content": prompt} 555 | ] 556 | } 557 | ) 558 | ``` 559 | 560 | ```console 561 | ❯ python 05_agent_langgraph.py 562 | Enter your prompt: Give me the sum of the values on transactions made to pam@dm.com 563 | sum_transactions 6 564 | -------------------------------------------------- 565 | {'messages': [HumanMessage(content='Give me the sum of the values on transactions made to pam@dm.com', additional_kwargs={}, response_metadata={}, id='32caee17-7f48-433b-9b0e-749a18d32b77'), AIMessage(content='', additional_kwargs={}, response_metadata={'model': 'qwen3:4b', 'created_at': '2025-05-23T20:18:33.625251234Z', 'done': True, 'done_reason': 'stop', 'total_duration': 27514733768, 'load_duration': 24980619, 'prompt_eval_count': 211, 'prompt_eval_duration': 4767770173, 'eval_count': 190, 'eval_duration': 22721368079, 'model_name': 'qwen3:4b'}, id='run--5716212a-0078-4322-9d16-6b3de2e9d3cd-0', tool_calls=[{'name': 'list_transactions', 'args': {'email': 'pam@dm.com'}, 'id': 'be9aaaa0-5c4c-45c1-bef5-c469982fde38', 'type': 'tool_call'}], usage_metadata={'input_tokens': 211, 'output_tokens': 190, 'total_tokens': 401}), ToolMessage(content='[{"date": "23/05/2025 18:55:17", "actor": "schrute@dundlermifflin.com", "value": 1.0, "email": "pam@dm.com", "name": "Pam Beasly", "dept": "General", "role": "Receptionist"}, {"date": "23/05/2025 12:49:11", "actor": "schrute@dundlermifflin.com", "value": 48.0, "email": "pam@dm.com", "name": "Pam Beasly", "dept": "General", "role": "Receptionist"}, {"date": "23/05/2025 12:40:34", "actor": "schrute@dundlermifflin.com", "value": 48.0, "email": "pam@dm.com", "name": "Pam Beasly", "dept": "General", "role": "Receptionist"}, {"date": "20/05/2025 20:21:12", "actor": "schrute@dundlermifflin.com", "value": 59.0, "email": "pam@dm.com", "name": "Pam Beasly", "dept": "General", "role": "Receptionist"}, {"date": "20/05/2025 20:21:07", "actor": "schrute@dundlermifflin.com", "value": 593.0, "email": "pam@dm.com", "name": "Pam Beasly", "dept": "General", "role": "Receptionist"}, {"date": "26/10/2024 15:41:57", "actor": "system", "value": 500.0, "email": "pam@dm.com", "name": "Pam Beasly", "dept": "General", "role": "Receptionist"}]', name='list_transactions', id='0590b717-1029-4f63-a728-6e70540ea299', tool_call_id='be9aaaa0-5c4c-45c1-bef5-c469982fde38'), AIMessage(content='', additional_kwargs={}, response_metadata={'model': 'qwen3:4b', 'created_at': '2025-05-23T20:19:53.287808928Z', 'done': True, 'done_reason': 'stop', 'total_duration': 78915230217, 'load_duration': 25010756, 'prompt_eval_count': 742, 'prompt_eval_duration': 34881159172, 'eval_count': 320, 'eval_duration': 43994892384, 'model_name': 'qwen3:4b'}, id='run--0710d81a-e7fa-4752-b4f9-47e7e1ed268a-0', tool_calls=[{'name': 'sum_transactions', 'args': {'transactions': [{'value': 1}, {'value': 48}, {'value': 48}, {'value': 59}, {'value': 593}, {'value': 500}]}, 'id': '16f80847-0898-4562-9b78-20ce48befd9c', 'type': 'tool_call'}], usage_metadata={'input_tokens': 742, 'output_tokens': 320, 'total_tokens': 1062}), ToolMessage(content='1249', name='sum_transactions', id='f803ba31-00d5-45ad-b36e-b91723d866df', tool_call_id='16f80847-0898-4562-9b78-20ce48befd9c'), AIMessage(content="\nOkay, let's see. The user asked for the sum of the values on transactions made to pam@dm.com. First, I needed to list those transactions using the list_transactions function. The response from that function gave me several transactions with different values. Then, I used the sum_transactions function to add up those values. The total sum came out to 1249. So, the final answer should be 1249. I need to make sure I didn't miss any transactions and that all the values were correctly included in the sum. Let me double-check the numbers: 1 + 48 is 49, plus another 48 is 97, then 59 makes 156, plus 593 is 749, and finally 500 brings it to 1249. Yep, that's correct.\n\n\nThe sum of the transactions for pam@dm.com is **1249**.", additional_kwargs={}, response_metadata={'model': 'qwen3:4b', 'created_at': '2025-05-23T20:20:26.125904592Z', 'done': True, 'done_reason': 'stop', 'total_duration': 32820032135, 'load_duration': 27163616, 'prompt_eval_count': 808, 'prompt_eval_duration': 4410864135, 'eval_count': 206, 'eval_duration': 28343256516, 'model_name': 'qwen3:4b'}, id='run--250d6c72-183c-42f7-be74-2b45b2db3df0-0', usage_metadata={'input_tokens': 808, 'output_tokens': 206, 'total_tokens': 1014})]} 566 | ``` 567 | 568 | ### Agente com PydanticAI 569 | 570 | ```py 571 | from pydantic_ai import Agent 572 | from pydantic_ai.agent import AgentRunResult 573 | from pydantic_ai.models.openai import OpenAIModel 574 | from pydantic_ai.providers.openai import OpenAIProvider 575 | 576 | # Set up Ollama LLM 577 | OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434") 578 | model = os.environ.get("MODEL", "qwen3:4b") 579 | model = OpenAIModel( 580 | model_name=model, 581 | provider=OpenAIProvider(base_url=f'{OLLAMA_URL}/v1') 582 | ) 583 | 584 | 585 | def list_transactions(email: str) -> list[dict]: 586 | ... 587 | 588 | def sum_transactions(transactions: List[Dict[str, Any]]) -> float: 589 | ... 590 | 591 | agent = Agent( 592 | model, 593 | tools=[list_transactions, sum_transactions], 594 | ) 595 | 596 | def invoke_agent(prompt: str) -> AgentRunResult: 597 | return agent.run_sync(prompt) 598 | ``` 599 | 600 | O resultado é tipado em um modelo Pydantic 601 | 602 | ```console 603 | ❯ python 06_agent_pydantic.py 604 | Enter your prompt: What is the sum of the amounts of all transactions for user 'pam@dm.com' 605 | -------------------------------------------------- 606 | AgentRunResult(output="\nOkay, let's see. The user asked for the sum of all transactions for the email 'pam@dm.com'. First, I need to get the list of transactions for that email. I'll call the list_transactions function with the email. Then, once I have the list, I'll sum the 'value' fields from each transaction using the sum_transactions function. Let me start by fetching the transactions.\n\nWait, the user already provided the transaction data in the tool_response. So I don't need to call the function again. The transactions are already there. Now I need to extract the 'value' from each entry and sum them up. Let me check the values: 1.0, 48.0, 48.0, 59.0, 593.0, and 500.0. Adding those together: 1 + 48 is 49, plus 48 is 97, then 59 makes 156, plus 593 is 749, plus 500 gives 1249. So the total sum should be 1249.0. I'll present that as the answer.\n\n\nThe sum of the amounts of all transactions for user 'pam@dm.com' is $1,249.00.") 607 | 608 | ``` 609 | 610 | O pydantic AI é esperto o suficiente para não invocar a tool mais de uma vez. 611 | E também é possível forçar o tipo nos retornos e inputs. 612 | 613 | ## MCP 614 | 615 | ### 🧠 O que é o MCP (Model Context Protocol)? 616 | 617 | O **Model Context Protocol (MCP)** é um padrão aberto desenvolvido pela **Anthropic** que permite que modelos de linguagem (como o Claude) se conectem de forma padronizada a ferramentas, dados e sistemas externos. Pense nele como um "USB-C da IA": uma forma universal de conectar modelos de IA a diferentes recursos, sem precisar de integrações personalizadas para cada caso. 618 | 619 | ### 🔧 Componentes principais 620 | 621 | * **Recursos (Resources)**: São dados ou informações que o modelo pode acessar, como arquivos, bancos de dados ou APIs. Por exemplo, um documento do Google Drive ou um repositório no GitHub. 622 | 623 | * **Ferramentas (Tools)**: São funções ou ações que o modelo pode executar, como enviar um e-mail, criar um pull request no GitHub ou consultar uma base de dados. Essas ferramentas são disponibilizadas por servidores MCP e podem ser utilizadas pelo modelo conforme necessário. 624 | 625 | * **Prompt**: É a entrada fornecida ao modelo, geralmente em linguagem natural, que pode incluir instruções específicas ou referências a recursos e ferramentas disponíveis. O modelo interpreta o prompt e decide como utilizar os recursos e ferramentas para gerar uma resposta adequada. 626 | 627 | ### 🧩 Como funciona na prática? 628 | 629 | Imagine que você está usando um assistente de IA para gerenciar tarefas no seu ambiente de trabalho. Com o MCP: 630 | 631 | 1. Você fornece um **prompt**, como: "Agende uma reunião com o time de vendas na próxima terça-feira às 10h." 632 | 633 | 2. O modelo analisa o prompt e identifica que precisa acessar o calendário da empresa (um **recurso**) e utilizar uma ferramenta de agendamento (uma **ferramenta**). 634 | 635 | 3. Utilizando o MCP, o modelo se conecta ao calendário, verifica a disponibilidade e agenda a reunião, tudo de forma padronizada e segura. 636 | 637 | ### ✅ Por que isso é útil? 638 | 639 | * **Padronização**: Desenvolvedores não precisam criar integrações específicas para cada ferramenta ou fonte de dados. 640 | 641 | * **Eficiência**: Modelos de IA podem acessar e utilizar recursos externos de forma mais rápida e eficaz. 642 | 643 | * **Segurança**: O MCP inclui mecanismos para garantir que o acesso a dados e ferramentas seja feito de forma controlada e segura. 644 | 645 | ### Modos de transporte 646 | 647 | Atualmente um servidor MCP pode ser iniciado em 2 modos, o modo stdio, que simplesmente executa uma chamada de função (como uma tool) 648 | ou em modo HTTP (usando SSE ou Streamable-HTTP) e dessa forma acessar APIs REST ou outros servers MCP. 649 | 650 | 651 | ### MCP Stdio 652 | 653 | ```py 654 | import os 655 | import json 656 | import subprocess 657 | from fastmcp import FastMCP 658 | 659 | mcp = FastMCP("Dundie CLI Stdio") 660 | 661 | @mcp.tool() 662 | def list_transactions(email: str) -> list[dict]: 663 | """List transactions for a given email.""" 664 | try: 665 | # Executa o comando da CLI 666 | result = subprocess.run( 667 | ["dundie", "list", "--email", email, "--asjson"], 668 | capture_output=True, 669 | text=True, 670 | check=True 671 | ) 672 | return json.loads(result.stdout.strip()) 673 | except subprocess.CalledProcessError as e: 674 | print(f"Failed to run command: {e}") 675 | return [] 676 | 677 | 678 | @mcp.tool() 679 | def add_transaction(email: str, value: int) -> str: 680 | """Add a transaction for a given email and value.""" 681 | try: 682 | # Executa o comando da CLI 683 | subprocess.run( 684 | ["dundie", "add", str(int(value)), "--email", email], 685 | capture_output=True, 686 | text=True, 687 | check=True, 688 | env={ 689 | "DUNDIE_EMAIL": "schrute@dundlermifflin.com", 690 | "DUNDIE_PASSWORD": "magic", 691 | **os.environ 692 | } 693 | ) 694 | return f"Transaction added successfully for {email} with value {value}" 695 | except subprocess.CalledProcessError as e: 696 | return f"Failed to add transaction for {email} with value {value} ({e})" 697 | 698 | 699 | if __name__ == "__main__": 700 | mcp.run(transport="stdio") 701 | ``` 702 | 703 | ### Agent + MCP Stdio com PydanticAI 704 | 705 | ```py 706 | import os 707 | import asyncio 708 | from typing import List, Dict, Any 709 | from pydantic_ai import Agent 710 | from pydantic_ai.agent import AgentRunResult 711 | from pydantic_ai.models.openai import OpenAIModel 712 | from pydantic_ai.providers.openai import OpenAIProvider 713 | from pydantic_ai.mcp import MCPServerStdio 714 | 715 | OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434") 716 | model = os.environ.get("MODEL", "qwen3:4b") 717 | model = OpenAIModel( 718 | model_name=model, 719 | provider=OpenAIProvider(base_url=f'{OLLAMA_URL}/v1') 720 | ) 721 | 722 | dundie_mcp = MCPServerStdio( 723 | "fastmcp", 724 | args=["run", "dundie_mcp.py"], 725 | ) 726 | 727 | def sum_transactions(transactions: List[Dict[str, Any]]) -> float: 728 | ... 729 | 730 | agent = Agent( 731 | model, 732 | tools=[sum_transactions], 733 | mcp_servers=[dundie_mcp] 734 | ) 735 | 736 | async def invoke_agent(prompt: str) -> AgentRunResult: 737 | return await agent.run(prompt) 738 | ``` 739 | 740 | 741 | ```console 742 | ❯ python 07_mcp_pydantic.py 743 | [05/23/25 23:49:59] INFO Starting MCP server 'Dundie CLI Stdio' with transport 'stdio' 744 | 745 | AgentRunResult(output="\nOkay, let's see. The user asked for the sum of all transactions for the user 'pam@dm.com'. First, I need to retrieve the list of transactions for that email. Looking at the tools provided, there's a function called list_transactions that takes an email as a parameter. So I should call that first to get the transaction data.\n\nWait, but the user already provided the transaction data in the tool_response. So maybe I don't need to call list_transactions again. The assistant already fetched the transactions. Now, the next step is to sum the 'value' fields of each transaction. The sum_transactions function is designed for that, taking an array of transactions. Each transaction has a 'value' property, so I can pass the list of transactions to sum_transactions to get the total. The result from the tool_response was 1249, which is the sum of all the values provided. So the final answer should be 1249.\n\n\nThe sum of the amounts of all transactions for user 'pam@dm.com' is **1249**.") 746 | ``` 747 | ```console 748 | ❯ python 07_mcp_pydantic.py 749 | [05/24/25 00:14:07] INFO Starting MCP server 'Dundie CLI Stdio' with transport 'stdio' server.py:747 750 | Enter your prompt: how many transactions for pam@dm.com? 751 | AgentRunResult(output='\nOkay, the user asked, "how many transactions for pam@dm.com?" So first, I need to figure out how to determine the number of transactions. The tools provided include list_transactions, which lists transactions for a given email. So I should call that function first to get the list of transactions for pam@dm.com.\n\nLooking at the tool response, there are six entries in the transactions list. Each entry is a separate transaction. So the count would be 6. But wait, let me double-check. The response from the tool shows six different dates and values, each with the same email. That means six transactions. Therefore, the answer is 6. I should present that clearly to the user.\n\n\nThere are 6 transactions for the email pam@dm.com.') 752 | ``` 753 | 754 | > [!WARNING] 755 | > Executar ações com efeitos colaterais como esse é uma brecha de segurança 756 | > exige cautela, validação e autenticação. 757 | 758 | ```console 759 | ❯ python 07_mcp_pydantic.py 760 | [05/24/25 00:32:37] INFO Starting MCP server 'Dundie CLI Stdio' with transport 'stdio' server.py:747 761 | Enter your prompt: add 9 points to pam@dm.com 762 | AgentRunResult(output="\nOkay, the user wanted to add 9 points to pam@dm.com. Let me check the available functions. There's add_transaction which takes email and value. The email is pam@dm.com and the value is 9. So I need to call add_transaction with those parameters. The response from the tool was successful, so I should confirm that the transaction was added. Let me make sure to mention both the email and the value in the response. Alright, that's all.\n\n\nThe transaction for email **pam@dm.com** with a value of **9** has been added successfully. Let me know if you need further assistance!") 763 | ``` 764 | ```console 765 | Enter your prompt: What is the total of the sum of all transactions of pam@dm.com 766 | AgentRunResult(output='\nOkay, let\'s see. The user asked for the total of all transactions for pam@dm.com. I need to calculate the sum of all the values in the provided transaction list.\n\nFirst, I\'ll check the tool responses. The tool_response has a list of transactions with dates, actors, values, and emails. The user\'s email is pam@dm.com, so I need to make sure all these transactions are indeed for that email. Looking through the entries, all of them have the email "pam@dm.com", so that\'s good.\n\nNext, I\'ll sum up all the \'value\' fields. Let\'s add them one by one:\n\n9.0 + 1.0 = 10.0\n\n10.0 + 1.0 = 11.0\n\n11.0 + 1.0 = 12.0\n\n12.0 + 48.0 = 60.0\n\n60.0 + 48.0 = 108.0\n\n108.0 + 59.0 = 167.0\n\n167.0 + 593.0 = 760.0\n\n760.0 + 500.0 = 1260.0\n\nSo the total sum is 1260. The tool_response also confirms the sum as 1260, which matches my calculation. Therefore, the answer is correct.\n\n\nThe total sum of all transactions for pam@dm.com is **1260**.') 767 | ``` 768 | 769 | ### Agent + MCP HTTP com PydanticAI 770 | 771 | Tudo igual, porem a conexão com MCP precisa de uma URL 772 | 773 | 774 | ```py 775 | MCP_URL = os.environ.get("MCP_URL", "http://localhost:8000/sse") 776 | dundie_mcp = MCPServerHTTP(MCP_URL) 777 | ``` 778 | 779 | E o servidor precisa ser executado. 780 | 781 | ```console 782 | ❯ fastmcp run dundie_mcp.py -t sse 783 | [05/24/25 00:47:24] INFO Starting MCP server 'Dundie CLI Stdio' with transport 'sse' on http://127.0.0.1:8000/sse server.py:796 784 | INFO: Started server process [667740] 785 | INFO: Waiting for application startup. 786 | INFO: Application startup complete. 787 | INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) 788 | INFO: 127.0.0.1:55490 - "GET /sse HTTP/1.1" 200 OK 789 | INFO: 127.0.0.1:55498 - "POST /messages/?session_id=a4a68291b5a94ee496fc7e161a51184b HTTP/1.1" 202 Accepted 790 | ``` 791 | 792 | E agora sim: 793 | 794 | ```console 795 | ❯ python 08_mcp_sse_pydantic.py 796 | Enter your prompt: Which actor transfered more points to pam@dm.com? 797 | 798 | AgentRunResult(output='\nOkay, let\'s see. The user is asking which actor transferred more points to pam@dm.com. The tool response provided a list of transactions. So, first, I need to look at the transactions and see who the actors are and the values they transferred.\n\nLooking at the data, the email is pam@dm.com, and the actor fields show names like schrute@dundlermifflin.com and system. The values are various numbers. The question is about which actor transferred more points. So, I need to sum the values for each actor.\n\nFirst, let\'s check the transactions. The actor "schrute@dundlermifflin.com" has multiple entries. Let me add up their values. The values are 9.0, 1.0, 1.0, 1.0, 48.0, 48.0, 59.0, 593.0. Adding those: 9 + 1 is 10, plus 1 is 11, plus 1 is 12, then 48 makes 60, another 48 is 108, 59 is 167, 593 is 760. So schrute\'s total is 760.\n\nThen there\'s the "system" actor with a value of 500.0. So system has 500. Comparing 760 and 500, schrute transferred more. Therefore, the answer is schrute@dundlermifflin.com.\n\n\nThe actor who transferred the most points to pam@dm.com is **schrute@dundlermifflin.com** with a total of **760.0 points**. The system actor transferred 500.0 points, which is less than schrute\'s total.') 799 | ``` 800 | -------------------------------------------------------------------------------- /context.csv: -------------------------------------------------------------------------------- 1 | Feature,Version,State 2 | User Authentication,1.0.2,stable 3 | Data Encryption,2.0.0,stable 4 | Cloud Synchronization,1.1.0,unstable 5 | Offline Mode,1.0.2,stable 6 | Multi-factor Authentication,1.1.0,stable 7 | Dark Theme,2.0.0,stable 8 | Data Export,1.0.2,unstable 9 | Automated Backups,1.1.0,unstable 10 | Realtime Collaboration,2.0.0,unstable 11 | Push Notifications,1.0.2,stable 12 | API Integration,1.1.0,stable 13 | Custom Reports,1.0.2,unstable 14 | Rolebased Access Control,2.0.0,stable 15 | Search Functionality,1.1.0,stable 16 | Performance Analytics,1.0.2,unstable 17 | Internationalization,2.0.0,unstable 18 | -------------------------------------------------------------------------------- /dundie_mcp.py: -------------------------------------------------------------------------------- 1 | """ 2 | Depends on dundie tool installed 3 | loaded and configured. 4 | 5 | https://github.com/rochacbruno/dundie-rewards 6 | """ 7 | 8 | import json 9 | import os 10 | import subprocess 11 | 12 | from fastmcp import FastMCP 13 | 14 | mcp = FastMCP("Dundie CLI Stdio") 15 | 16 | 17 | @mcp.tool() 18 | def list_transactions(email: str) -> list[dict]: 19 | """List transactions for a given email.""" 20 | try: 21 | # Executa o comando da CLI 22 | result = subprocess.run( 23 | ["dundie", "list", "--email", email, "--asjson"], 24 | capture_output=True, 25 | text=True, 26 | check=True, 27 | ) 28 | return json.loads(result.stdout.strip()) 29 | except subprocess.CalledProcessError as e: 30 | print(f"Failed to run command: {e}") 31 | return [] 32 | 33 | 34 | @mcp.tool() 35 | def add_transaction(email: str, value: int) -> str: 36 | """Add a transaction for a given email and value.""" 37 | try: 38 | # Executa o comando da CLI 39 | subprocess.run( 40 | ["dundie", "add", str(int(value)), "--email", email], 41 | capture_output=True, 42 | text=True, 43 | check=True, 44 | env={ 45 | "DUNDIE_EMAIL": "schrute@dundlermifflin.com", 46 | "DUNDIE_PASSWORD": "magic", 47 | **os.environ, 48 | }, 49 | ) 50 | return f"Transaction added successfully for {email} with value {value}" 51 | except subprocess.CalledProcessError as e: 52 | return f"Failed to add transaction for {email} with value {value} ({e})" 53 | 54 | 55 | if __name__ == "__main__": 56 | mcp.run(transport="stdio") 57 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | requests 3 | 4 | langchain 5 | langchain-ollama 6 | langchain-community 7 | langgraph 8 | fastmcp 9 | pydantic-ai-slim[openai,mcp] 10 | uvicorn 11 | sse-starlette 12 | 13 | python-lsp-server[all] 14 | ipython 15 | ipdb 16 | --------------------------------------------------------------------------------