Agente de cálculo
Un agente con una calculadora segura (AST, sin `eval`) y un conversor de unidades que los encadena para resolver problemas de palabras. El agente planea; los tools hacen la aritmética y las conversiones exactas.
Showcase — Agente de cálculo
Un agente con una calculadora segura (AST, sin eval) y un conversor de unidades que los encadena para resolver problemas de palabras. El agente planea; los tools hacen la aritmética y las conversiones exactas.
Córrelo
bash bootstrap-secrets.sh # reads ../../../../.env, writes secrets/
docker compose up --build # default: PROVIDER=openai
Abre http://localhost:3000. Gemini: PROVIDER=gemini docker compose up --build.
Qué hay aquí
backend/ai_openai.py/backend/ai_gemini.py— los dos tools + el agent loop acotado.frontend/app/page.tsx— caja de pregunta, respuesta encadenada.
Detenlo
docker compose down
Ejecútalo en tu máquina
Descarga el proyecto como ZIP y córrelo con Docker. Levanta un backend FastAPI y un frontend Next.js en localhost:3000.
unzip calc-agent.zip
cd calc-agent
bash bootstrap-secrets.sh # one-time: pulls API keys into ./secrets
docker compose up --build # default provider: openai
# or: PROVIDER=gemini docker compose up --build
Escribe algo, elige un proveedor y ejecuta el mismo código de Código contra la API real. Requiere iniciar sesión.
Los mismos módulos que ejecuta el botón Run. El proyecto completo (frontend, Dockerfile, compose) está en el ZIP, pestaña README.
backend/ai_openai.py
"""Showcase 1 (OpenAI): a calculation agent.
An agent with two tools — a safe calculator and a unit converter — that solves
word problems by chaining them. "How many pounds is 12 kg, and what's that times
3?" takes a convert then a calculate. The agent plans the chain; the tools do the
exact work models are bad at.
"""
import ast
import json
import operator
from openai import OpenAI
_client = OpenAI()
_MODEL = "gpt-5.4-nano"
_OPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
ast.Div: operator.truediv, ast.Pow: operator.pow, ast.USub: operator.neg}
def _eval(node):
if isinstance(node, ast.Constant):
return node.value
if isinstance(node, ast.BinOp):
return _OPS[type(node.op)](_eval(node.left), _eval(node.right))
if isinstance(node, ast.UnaryOp):
return _OPS[type(node.op)](_eval(node.operand))
raise ValueError("unsupported expression")
def calculate(expression: str) -> dict:
try:
return {"result": _eval(ast.parse(expression, mode="eval").body)}
except Exception as e:
return {"error": str(e)}
_FACTORS = {("km", "mi"): 0.621371, ("mi", "km"): 1.60934, ("kg", "lb"): 2.20462,
("lb", "kg"): 0.453592, ("m", "ft"): 3.28084, ("ft", "m"): 0.3048}
def convert(value: float, from_unit: str, to_unit: str) -> dict:
f, t = from_unit.lower(), to_unit.lower()
if (f, t) == ("c", "f"):
return {"result": value * 9 / 5 + 32}
if (f, t) == ("f", "c"):
return {"result": (value - 32) * 5 / 9}
factor = _FACTORS.get((f, t))
if factor is None:
return {"error": f"no conversion {f}->{t}", "known": [f"{a}->{b}" for a, b in _FACTORS]}
return {"result": value * factor}
_IMPL = {"calculate": calculate, "convert": convert}
_TOOLS = [
{"type": "function", "name": "calculate",
"description": "Evaluate arithmetic like '12.5 * 3'. Supports + - * / ** and parentheses.",
"parameters": {"type": "object", "properties": {"expression": {"type": "string"}}, "required": ["expression"]}},
{"type": "function", "name": "convert",
"description": "Convert a value between units: km/mi, kg/lb, m/ft, c/f.",
"parameters": {"type": "object", "properties": {
"value": {"type": "number"}, "from_unit": {"type": "string"}, "to_unit": {"type": "string"}},
"required": ["value", "from_unit", "to_unit"]}},
]
_GOAL = ("You are a calculation agent. Use `convert` for unit conversions and "
"`calculate` for arithmetic. Never compute in your head. Answer plainly with units.")
def run(question: str) -> str:
q = question.strip()
if not q:
return "Ask a math or unit-conversion question."
input_list = [{"role": "user", "content": q}]
response = None
for _ in range(8):
response = _client.responses.create(model=_MODEL, instructions=_GOAL, input=input_list, tools=_TOOLS)
calls = [i for i in response.output if i.type == "function_call"]
if not calls:
break
input_list += response.output
for c in calls:
result = _IMPL[c.name](**json.loads(c.arguments))
input_list.append({"type": "function_call_output", "call_id": c.call_id, "output": json.dumps(result)})
return response.output_text if response else ""
backend/ai_gemini.py
"""Showcase 1 (Gemini): a calculation agent.
Same calculator + converter tools and bounded loop, Gemini's tool-calling.
"""
import ast
import operator
import os
from google import genai
from google.genai import types
_client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
_MODEL = "gemini-3.1-flash-lite"
_OPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
ast.Div: operator.truediv, ast.Pow: operator.pow, ast.USub: operator.neg}
def _eval(node):
if isinstance(node, ast.Constant):
return node.value
if isinstance(node, ast.BinOp):
return _OPS[type(node.op)](_eval(node.left), _eval(node.right))
if isinstance(node, ast.UnaryOp):
return _OPS[type(node.op)](_eval(node.operand))
raise ValueError("unsupported expression")
def calculate(expression: str) -> dict:
try:
return {"result": _eval(ast.parse(expression, mode="eval").body)}
except Exception as e:
return {"error": str(e)}
_FACTORS = {("km", "mi"): 0.621371, ("mi", "km"): 1.60934, ("kg", "lb"): 2.20462,
("lb", "kg"): 0.453592, ("m", "ft"): 3.28084, ("ft", "m"): 0.3048}
def convert(value: float, from_unit: str, to_unit: str) -> dict:
f, t = from_unit.lower(), to_unit.lower()
if (f, t) == ("c", "f"):
return {"result": value * 9 / 5 + 32}
if (f, t) == ("f", "c"):
return {"result": (value - 32) * 5 / 9}
factor = _FACTORS.get((f, t))
if factor is None:
return {"error": f"no conversion {f}->{t}"}
return {"result": value * factor}
_IMPL = {"calculate": calculate, "convert": convert}
_CONFIG = types.GenerateContentConfig(
system_instruction=("You are a calculation agent. Use `convert` for unit conversions "
"and `calculate` for arithmetic. Never compute in your head. Answer with units."),
tools=[types.Tool(function_declarations=[
types.FunctionDeclaration(name="calculate", description="Evaluate arithmetic like '12.5 * 3'.",
parameters=types.Schema(type=types.Type.OBJECT,
properties={"expression": types.Schema(type=types.Type.STRING)}, required=["expression"])),
types.FunctionDeclaration(name="convert", description="Convert a value between units: km/mi, kg/lb, m/ft, c/f.",
parameters=types.Schema(type=types.Type.OBJECT, properties={
"value": types.Schema(type=types.Type.NUMBER),
"from_unit": types.Schema(type=types.Type.STRING),
"to_unit": types.Schema(type=types.Type.STRING)}, required=["value", "from_unit", "to_unit"])),
])],
)
def run(question: str) -> str:
q = question.strip()
if not q:
return "Ask a math or unit-conversion question."
contents = [types.Content(role="user", parts=[types.Part(text=q)])]
response = None
for _ in range(8):
response = _client.models.generate_content(model=_MODEL, contents=contents, config=_CONFIG)
if not response.function_calls:
break
contents.append(response.candidates[0].content)
parts = [types.Part.from_function_response(name=fc.name, response=_IMPL[fc.name](**dict(fc.args)))
for fc in response.function_calls]
contents.append(types.Content(role="user", parts=parts))
return (response.text or "") if response else ""
Archivos del proyecto
.gitignoreREADME.es.mdREADME.mdbackend/Dockerfilebackend/ai_gemini.pybackend/ai_openai.pybackend/main.pybackend/requirements.txtbootstrap-secrets.shdocker-compose.ymlfrontend/Dockerfilefrontend/app/layout.tsxfrontend/app/page.tsxfrontend/next.config.tsfrontend/package.jsonfrontend/tsconfig.json