Acesse https://ollama.com/download para baixar e instalar o Ollama.
Se estiver no Linux pode usar o seguinte comando:
curl -fsSL https://ollama.com/install.sh | sh
Uma vez instalado certifique de que o servidor ollama está rodando:
ollama serve
Mantenha este terminal aberto para que o servidor continue rodando. Se a mensagem informar que já está rodando, pode ignorar este passo.
Para instalar novos modelos:
ollama install <modelo>
Verifique os modelos disponíveis no link https://ollama.com/models
Usaremos os seguintes modelos:
[!INFO] Verifique o quanto possui de espaço livre em disco e memória RAM antes de instalar os modelos, 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.
ollama pull qwen3:0.6b
ollama pull qwen3:4b
ollama pull llama3.1
ollama pull gemma3:12b
Para listar os modelos disponíveis:
ollama ls
Clone este repositório:
git clone https://github.com/rochacbruno/python-base-ai
cd python-base-ai
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.
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.
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.
Nosso foco aqui é extritamente em como, no papel de pessoa que desenvolve programas com Python, você pode integrar com esses modelos.
- 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.
- 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.
- Prompt: Uma sequência de texto que é enviada para o modelo para que ele gere uma resposta.
- Token: Um token é uma unidade de medida usada para medir a quantidade de texto que um modelo pode processar.
- 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.
- 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.
- 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.
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, e algumas maneiras de interação como APIS de generate, chat e completion.
No Ollama temos algumas APIs interessantes:
Listar os models:
curl -X GET http://localhost:11434/api/tags
{"models":[{"name":"llama3.1:latest","model":"llama3.1:latest"}, {"name":"llama2:latest","model":"llama2:latest"}]}
Generate é uma API usada para gerar texto a partir de um prompt.
curl -X POST http://localhost:11434/api/generate -H "Content-Type: application/json" -d '{"prompt":"Hello"}'
{"id":"1234567890","model":"llama3.1:latest","prompt":"Hello","completion":"Hello, how are you?"}
Chat é uma API usada para interagir com um modelo de AI Generativa em um contexto de conversa, a diferença é que esta API mantém contexto entre as chamadas.
curl -X POST http://localhost:11434/api/chat -H "Content-Type: application/json" -d '{"prompt":"Hello"}'
{"id":"1234567890","model":"llama3.1:latest","prompt":"Hello","completion":"Hello, how are you?"}
Para mais endpoints, visite a documentação oficial do Ollama.
curl -X POST http://localhost:11434/api/generate -H "Content-Type: application/json" -d '{"prompt":"Hello", "model": "qwen3:0.6b", "stream": false}'
{
"model":"qwen3:0.6b",
"created_at":"2025-05-23T18:25:46.508616186Z",
"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
Hello! How can I assist you today? 😊 Have a great day!",
"done":true,
"done_reason":"stop",
"context":[],
"total_duration":2556592493,
"load_duration":25473441,
"prompt_eval_count":9,
"prompt_eval_duration":22163075,
"eval_count":112,
"eval_duration":2508279738
}
Para começar crie um ambiente virtual na raiz do repositório.
python3 -m venv .venv
Ative o ambiente
source .venv/bin/activate
Instale os requisitos
pip install -r requirements.txt
Da mesma forma que usamos CURL para interagir com o modelo, podemos usar a biblioteca Requests para fazer chamadas HTTP.
O funcionamento é bastante similar, o código abaixo é auto-explicativo leia linha a linha:
def ask_ollama(prompt: str):
url = f"{OLLAMA_URL}/api/chat"
headers = {
"Content-Type": "application/json",
"accept": "application/json"
}
model = "qwen3:0.6b"
system_prompt = "Answer in Portuguese" # Aqui dá para ser criativo e personalizar o comportamento do modelo
payload = {
"model": model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
]
}
response = requests.post(url, headers=headers, json=payload, stream=True)
# A resposta é um gerador de chunks pois o Ollama retorna com SSE por default
# portanto precisamos iterar sobre os chunks e processá-los
for line in response.iter_lines():
if line:
data = json.loads(line.decode("utf-8"))
message = data.get("message", {}).get("content", "")
if message:
print(message, end="")
O programa completo está no exemplo 01_simple_client.py
Execute:
[!INFO] 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.
python3 01_simple_client.py
Exemplo:
❯ python 01_simple_client.py
Enter your prompt: What is a Barometer?
<think>
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.
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.
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.
</think>
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.
Tip
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 <think>
colocando o comando /no_think
no system prompt.
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.
import ollama
def ask_ollama(prompt):
client = ollama.Client(host=OLLAMA_URL)
model = "qwen3:0.6b"
system_prompt = "Answer in Portuguese"
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
]
stream = client.chat(
model=model,
messages=messages,
stream=True
)
for chunk in stream:
message_chunk = chunk.get('message', {}).get('content', '')
if message_chunk:
print(message_chunk, end="")
print()
O código completo está no exemplo 02_ollama_client.py
❯ python 02_ollama_client.py
Enter your prompt: Qual é a raiz quadrada de 81?
<think>
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.
</think>
A raíz quadrada de 81 é $ \sqrt{81} = 9 $.
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 é antigo, ou pelo menos com alguns meses de defasagem, pois demora bastante tempo para treinar um modelo com bilhões de parâmetros.
Portanto para respostas mais atualizadas precisamos fornecer contexto extra, e existem várias abordagens para fornecer contexto, a mais simples é fornecer o contexto no próprio prompt, é bastante usada em interações com chatbots, passando pelo uso de assistant prompts 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 tools, MCP e RAG.
Vamos abordar os 3 primeiros
- injeção de contexto no prompt - que possui limitações principalmente no tamanho dos tokens
- tools - Que é uma padronização para disponibilizar funções que o modelo invoca através do agente
- MCP - QUe é uma padronização para criação de APIs que fornecem contexto
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.
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, 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 pagaremos mais se injetarmos muito contexto dessa forma.
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, o nosso contexto é o seguinte:
# Features Included in Each of our software versions
Here is the list of features included in the most
recent versions of our Magic Software.
Feature,Version,State
User Authentication,1.0.2,stable
Data Encryption,2.0.0,stable
Cloud Synchronization,1.1.0,unstable
Offline Mode,1.0.2,stable
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.
Alguns pontos importantes:
- Quanto menos tokens melhor, portanto é melhor um CSV do que um JSON
- Use https://huggingface.co/spaces/Xenova/the-tokenizer-playground para calcular
- É preciso contextualizar, portanto tem que ter headers e no prompt final incluir text instrutivo
Como vamos passar esse contexto para o LLM?
CONTEXT = open("context.csv").read()
def ask_ollama(prompt):
client = ollama.Client(host=OLLAMA_URL)
model = "llama3.1"
system_prompt = "Analize the assistant content and give short answers"
messages = [
{"role": "system", "content": system_prompt},
# Context Injection:
{"role": "assistant", "content": f"Here is the list of feature, version and state for all the Magic System features.\n {CONTEXT}"},
{"role": "user", "content": prompt},
]
stream = client.chat(
model=model,
messages=messages,
stream=True
)
[!INFO] 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, isso significa que a reposta será mais demorada e vai consumir pelo menos 8GB de RAM.
Agora podemos fazer perguntas mais elaboradas e contando com o contexto injetado:
❯ python 03_context_client.py
Enter your prompt: Which versions did we released?
We released:
1. Version 1.0.2 (with features: User Authentication, Data Encryption, Offline Mode, Push Notifications)
2. Version 1.1.0 (with features: Cloud Synchronization, Multi-factor Authentication)
3. Version 2.0.0 (with features: Dark Theme, Realtime Collaboration, API Integration)
--------------------------------------------------
Enter your prompt:
❯ python 03_context_client.py
Enter your prompt: Create a promotional marketing message covering the stable features of version 2.0.0
Here's a promotional marketing message:
**Introducing Magic System 2.0.0: Unlock Unparalleled Security and Collaboration!**
Take your productivity to new heights with our latest update, featuring a robust suite of stable features designed to revolutionize the way you work.
**Unmatched Security**
* **Data Encryption**: Protect your sensitive information with top-notch encryption methods.
* **Multi-factor Authentication**: Add an extra layer of security to prevent unauthorized access.
* **Role-based Access Control**: Limit user permissions to ensure data integrity and accountability.
**Collaborate Seamlessly**
* **Dark Theme**: Improve focus and reduce eye strain with our sleek dark mode.
* **API Integration**: Integrate Magic System with your favorite apps for effortless workflow management.
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!
[!INFO] Quanto mais otimizado o modelo, melhor será a reposta.
Até aqui falamos de clients, que são formas passivas de interagir com LLMs, mandamos um prompt e a AI responde.
Agentes são uma outra categoria de clients, sim, eles ainda são clientes, mas possuem 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.
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.
Aqui pretendo abordar o cenário atual, dentro das pesquisas que fui capaz de fazer.
Abordaremos agentes simples que terão dois objetivos:
- Fornecer contexto dinamicamente
- Executar ações a partir da resposta da AI
Esses agentes que criaremos irão funcionar a partir da invocação de um prompt humano, ou seja, teremos que pedir.
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.
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.
O protocolo de tools é um dos principais motivos para usarmos agentes, ele permite que registremos funções que serão invocadas pelo agente, sempre que o modelo necessitar de contexto adicional ou quiser efetuar uma ação.
Não são todos os modelos que suportam o uso de tools, no caso do Ollama podemos ver a lista de modelos, e um outro detalhe é que para conseguir decidir o uso de tools o modelo precisa ter mais parametros, nos meus testes, apenas modelos com 8b ou mais conseguem usar tools de forma eficiente.
Modelos com suporte a tools. https://ollama.com/search?c=tools
Lembra que criamos um programa chamado dundie
que controlar uma conta corrente de pontos de funcionários?
E se criarmos um agente de AI para consultarmos os pontos, e talvez um para adicionar pontos?
[!INFO] Para conseguir selecionar tools com langchain o modelo tem que ser pelo menos de 12b com suporte a tools, testei todos os outros como llama3.1:8b e nenhum foi capaz de encontrar as tools.
model = os.environ.get("MODEL", "gemma3:12b")
llm = OllamaLLM(model=model, base_url=OLLAMA_URL)
def list_transactions(email: str) -> list[dict]:
"""List user transaction by email"""
try:
result = subprocess.run(
["dundie", "list", "--email", email, "--asjson"],
capture_output=True,
text=True,
check=True
)
return json.loads(result.stdout.strip())
except subprocess.CalledProcessError as e:
print(f"Error listing transactions: {e}")
return []
tools = [
Tool(
name="list_transactions",
func=list_transactions,
description="Use this when you need to list user transactions."
)
]
agent = initialize_agent(
llm=llm,
tools=tools,
verbose=True,
agent_type="zero-shot-react-description"
)
❯ python 04_agent_langchain.py
Enter your prompt: How many transactions for [email protected]?
> Entering new AgentExecutor chain...
I need to list the transactions for [email protected] to determine the number of transactions.
Action: list_transactions
Action Input: [email protected]
Observation: [{'date': '23/05/2025 18:55:17', 'actor': '[email protected]', 'value': 1.0, 'email': '[email protected]', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}, {'date': '23/05/2025 12:49:11', 'actor': '[email protected]', 'value': 48.0, 'email': '[email protected]', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}, {'date': '23/05/2025 12:40:34', 'actor': '[email protected]', 'value': 48.0, 'email': '[email protected]', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}, {'date': '20/05/2025 20:21:12', 'actor': '[email protected]', 'value': 59.0, 'email': '[email protected]', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}, {'date': '20/05/2025 20:21:07', 'actor': '[email protected]', 'value': 593.0, 'email': '[email protected]', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}, {'date': '26/10/2024 15:41:57', 'actor': 'system', 'value': 500.0, 'email': '[email protected]', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}]
Thought:I have listed the transactions for [email protected] and there are 6 transactions.
Final Answer: 6
> Finished chain.
--------------------------------------------------
{'input': 'How many transactions for [email protected]?', 'output': '6'}
❯ python 04_agent_langchain.py
Enter your prompt: What is the total sum of values that [email protected] received?
> Entering new AgentExecutor chain...
Okay, I need to find the transactions associated with the email address "[email protected]" and then sum the values received in those transactions. To do this, I need to use the `list_transactions` tool.
Action: list_transactions
Action Input: [email protected]
Observation: [{'date': '23/05/2025 18:55:17', 'actor': '[email protected]', 'value': 1.0, 'email': '[email protected]', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}, {'date': '23/05/2025 12:49:11', 'actor': '[email protected]', 'value': 48.0, 'email': '[email protected]', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}, {'date': '23/05/2025 12:40:34', 'actor': '[email protected]', 'value': 48.0, 'email': '[email protected]', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}, {'date': '20/05/2025 20:21:12', 'actor': '[email protected]', 'value': 59.0, 'email': '[email protected]', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}, {'date': '20/05/2025 20:21:07', 'actor': '[email protected]', 'value': 593.0, 'email': '[email protected]', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}, {'date': '26/10/2024 15:41:57', 'actor': 'system', 'value': 500.0, 'email': '[email protected]', 'name': 'Pam Beasly', 'dept': 'General', 'role': 'Receptionist'}]
Thought:I have the list of transactions for [email protected]. 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.
1.0 + 48.0 + 48.0 + 59.0 + 593.0 + 500.0 = 1249.0
Thought: I now know the final answer
Final Answer: 1249.0
> Finished chain.
--------------------------------------------------
{'input': 'What is the total sum of values that [email protected] received?', 'output': '1249.0'}
Tudo parecido, porém aparentemente o LangGraph é melhor em contextualizar o modelo, portanto podemos até usar um modelo menor.
As tools sao registradas via decorator.
from langchain_ollama import ChatOllama
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
model = os.environ.get("MODEL", "qwen3:4b")
llm = ChatOllama(model=model, base_url=OLLAMA_URL)
@tool
def list_transactions(email: str) -> list[dict]:
"""List user transaction by email"""
try:
# Executa o comando da CLI
result = subprocess.run(
["dundie", "list", "--email", email, "--asjson"],
capture_output=True,
text=True,
check=True
)
return json.loads(result.stdout.strip())
except subprocess.CalledProcessError as e:
print(f"Error listing transactions: {e}")
return []
@tool
def sum_transactions(transactions: list[dict]) -> float:
"""
Sum the values of a list of transactions.
"""
print("sum_transactions", len(transactions))
return sum([t["value"] for t in transactions])
# Create the agent
agent = create_react_agent(
llm,
tools=[list_transactions, sum_transactions],
)
def invoke_agent(prompt: str) -> dict:
return agent.invoke(
{
"messages": [
{"role": "user",
"content": prompt}
]
}
)
❯ python 05_agent_langgraph.py
Enter your prompt: Give me the sum of the values on transactions made to [email protected]
sum_transactions 6
--------------------------------------------------
{'messages': [HumanMessage(content='Give me the sum of the values on transactions made to [email protected]', 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': '[email protected]'}, '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": "[email protected]", "value": 1.0, "email": "[email protected]", "name": "Pam Beasly", "dept": "General", "role": "Receptionist"}, {"date": "23/05/2025 12:49:11", "actor": "[email protected]", "value": 48.0, "email": "[email protected]", "name": "Pam Beasly", "dept": "General", "role": "Receptionist"}, {"date": "23/05/2025 12:40:34", "actor": "[email protected]", "value": 48.0, "email": "[email protected]", "name": "Pam Beasly", "dept": "General", "role": "Receptionist"}, {"date": "20/05/2025 20:21:12", "actor": "[email protected]", "value": 59.0, "email": "[email protected]", "name": "Pam Beasly", "dept": "General", "role": "Receptionist"}, {"date": "20/05/2025 20:21:07", "actor": "[email protected]", "value": 593.0, "email": "[email protected]", "name": "Pam Beasly", "dept": "General", "role": "Receptionist"}, {"date": "26/10/2024 15:41:57", "actor": "system", "value": 500.0, "email": "[email protected]", "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="<think>\nOkay, let's see. The user asked for the sum of the values on transactions made to [email protected]. 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</think>\n\nThe sum of the transactions for [email protected] 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})]}
from pydantic_ai import Agent
from pydantic_ai.agent import AgentRunResult
from pydantic_ai.models.openai import OpenAIModel
from pydantic_ai.providers.openai import OpenAIProvider
# Set up Ollama LLM
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
model = os.environ.get("MODEL", "qwen3:4b")
model = OpenAIModel(
model_name=model,
provider=OpenAIProvider(base_url=f'{OLLAMA_URL}/v1')
)
def list_transactions(email: str) -> list[dict]:
...
def sum_transactions(transactions: List[Dict[str, Any]]) -> float:
...
agent = Agent(
model,
tools=[list_transactions, sum_transactions],
)
def invoke_agent(prompt: str) -> AgentRunResult:
return agent.run_sync(prompt)
O resultado é tipado em um modelo Pydantic
❯ python 06_agent_pydantic.py
Enter your prompt: What is the sum of the amounts of all transactions for user '[email protected]'
--------------------------------------------------
AgentRunResult(output="<think>\nOkay, let's see. The user asked for the sum of all transactions for the email '[email protected]'. 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</think>\n\nThe sum of the amounts of all transactions for user '[email protected]' is $1,249.00.")
O pydantic AI é esperto o suficiente para não invocar a tool mais de uma vez. E também é possível forçar o tipo nos retornos e inputs.
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.
-
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.
-
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.
-
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.
Imagine que você está usando um assistente de IA para gerenciar tarefas no seu ambiente de trabalho. Com o MCP:
-
Você fornece um prompt, como: "Agende uma reunião com o time de vendas na próxima terça-feira às 10h."
-
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).
-
Utilizando o MCP, o modelo se conecta ao calendário, verifica a disponibilidade e agenda a reunião, tudo de forma padronizada e segura.
-
Padronização: Desenvolvedores não precisam criar integrações específicas para cada ferramenta ou fonte de dados.
-
Eficiência: Modelos de IA podem acessar e utilizar recursos externos de forma mais rápida e eficaz.
-
Segurança: O MCP inclui mecanismos para garantir que o acesso a dados e ferramentas seja feito de forma controlada e segura.
Atualmente um servidor MCP pode ser iniciado em 2 modos, o modo stdio, que simplesmente executa uma chamada de função (como uma tool) ou em modo HTTP (usando SSE ou Streamable-HTTP) e dessa forma acessar APIs REST ou outros servers MCP.
import os
import json
import subprocess
from fastmcp import FastMCP
mcp = FastMCP("Dundie CLI Stdio")
@mcp.tool()
def list_transactions(email: str) -> list[dict]:
"""List transactions for a given email."""
try:
# Executa o comando da CLI
result = subprocess.run(
["dundie", "list", "--email", email, "--asjson"],
capture_output=True,
text=True,
check=True
)
return json.loads(result.stdout.strip())
except subprocess.CalledProcessError as e:
print(f"Failed to run command: {e}")
return []
@mcp.tool()
def add_transaction(email: str, value: int) -> str:
"""Add a transaction for a given email and value."""
try:
# Executa o comando da CLI
subprocess.run(
["dundie", "add", str(int(value)), "--email", email],
capture_output=True,
text=True,
check=True,
env={
"DUNDIE_EMAIL": "[email protected]",
"DUNDIE_PASSWORD": "magic",
**os.environ
}
)
return f"Transaction added successfully for {email} with value {value}"
except subprocess.CalledProcessError as e:
return f"Failed to add transaction for {email} with value {value} ({e})"
if __name__ == "__main__":
mcp.run(transport="stdio")
import os
import asyncio
from typing import List, Dict, Any
from pydantic_ai import Agent
from pydantic_ai.agent import AgentRunResult
from pydantic_ai.models.openai import OpenAIModel
from pydantic_ai.providers.openai import OpenAIProvider
from pydantic_ai.mcp import MCPServerStdio
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
model = os.environ.get("MODEL", "qwen3:4b")
model = OpenAIModel(
model_name=model,
provider=OpenAIProvider(base_url=f'{OLLAMA_URL}/v1')
)
dundie_mcp = MCPServerStdio(
"fastmcp",
args=["run", "dundie_mcp.py"],
)
def sum_transactions(transactions: List[Dict[str, Any]]) -> float:
...
agent = Agent(
model,
tools=[sum_transactions],
mcp_servers=[dundie_mcp]
)
async def invoke_agent(prompt: str) -> AgentRunResult:
return await agent.run(prompt)
❯ python 07_mcp_pydantic.py
[05/23/25 23:49:59] INFO Starting MCP server 'Dundie CLI Stdio' with transport 'stdio'
AgentRunResult(output="<think>\nOkay, let's see. The user asked for the sum of all transactions for the user '[email protected]'. 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</think>\n\nThe sum of the amounts of all transactions for user '[email protected]' is **1249**.")
❯ python 07_mcp_pydantic.py
[05/24/25 00:14:07] INFO Starting MCP server 'Dundie CLI Stdio' with transport 'stdio' server.py:747
Enter your prompt: how many transactions for [email protected]?
AgentRunResult(output='<think>\nOkay, the user asked, "how many transactions for [email protected]?" 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 [email protected].\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</think>\n\nThere are 6 transactions for the email [email protected].')
Warning
Executar ações com efeitos colaterais como esse é uma brecha de segurança exige cautela, validação e autenticação.
❯ python 07_mcp_pydantic.py
[05/24/25 00:32:37] INFO Starting MCP server 'Dundie CLI Stdio' with transport 'stdio' server.py:747
Enter your prompt: add 9 points to [email protected]
AgentRunResult(output="<think>\nOkay, the user wanted to add 9 points to [email protected]. Let me check the available functions. There's add_transaction which takes email and value. The email is [email protected] 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</think>\n\nThe transaction for email **[email protected]** with a value of **9** has been added successfully. Let me know if you need further assistance!")
Enter your prompt: What is the total of the sum of all transactions of [email protected]
AgentRunResult(output='<think>\nOkay, let\'s see. The user asked for the total of all transactions for [email protected]. 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 [email protected], so I need to make sure all these transactions are indeed for that email. Looking through the entries, all of them have the email "[email protected]", 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</think>\n\nThe total sum of all transactions for [email protected] is **1260**.')
Tudo igual, porem a conexão com MCP precisa de uma URL
MCP_URL = os.environ.get("MCP_URL", "http://localhost:8000/sse")
dundie_mcp = MCPServerHTTP(MCP_URL)
E o servidor precisa ser executado.
❯ fastmcp run dundie_mcp.py -t sse
[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
INFO: Started server process [667740]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: 127.0.0.1:55490 - "GET /sse HTTP/1.1" 200 OK
INFO: 127.0.0.1:55498 - "POST /messages/?session_id=a4a68291b5a94ee496fc7e161a51184b HTTP/1.1" 202 Accepted
E agora sim:
❯ python 08_mcp_sse_pydantic.py
Enter your prompt: Which actor transfered more points to [email protected]?
AgentRunResult(output='<think>\nOkay, let\'s see. The user is asking which actor transferred more points to [email protected]. 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 [email protected], and the actor fields show names like [email protected] 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 "[email protected]" 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 [email protected].\n</think>\n\nThe actor who transferred the most points to [email protected] is **[email protected]** with a total of **760.0 points**. The system actor transferred 500.0 points, which is less than schrute\'s total.')