Files
2026-07-13 13:35:10 +08:00

410 lines
14 KiB
Python

import json
import logging
import os
from dataclasses import asdict, dataclass
from datetime import datetime
from typing import Any, Dict, Optional
import openai
SYSTEM_MESSAGE = """You are a mathematical problem-solving agent. You can only use these four atomic tools to solve problems:
- add(a, b): Add two numbers
- sub(a, b): Subtract b from a
- mul(a, b): Multiply two numbers
- div(a, b): Divide a by b
Your task is to break down complex mathematical expressions into a sequence of these atomic operations, following proper order of operations (parentheses, multiplication/division, addition/subtraction).
For each step, call the appropriate tool with the correct arguments. Work step by step, showing your reasoning.
When you have the final answer, respond with just the number."""
@dataclass
class TraceEvent:
"""Single event in the application trace"""
event_type: (
str # "llm_call", "tool_execution", "error", "init", "result_extraction"
)
component: str # "openai_api", "math_tools", "agent", "parser"
data: Dict[str, Any]
@dataclass
class ToolResult:
tool_name: str
args: Dict[str, float]
result: float
step_number: int
class MathToolsAgent:
def __init__(
self,
client,
model_name: str = "gpt-4o",
system_message: str = SYSTEM_MESSAGE,
logdir: str = "logs",
):
"""
Initialize the LLM agent with OpenAI API
Args:
client: OpenAI client instance
model_name: Name of the model to use
system_message: System message for the agent
logdir: Directory to save trace logs
"""
self.client = client
self.system_message = system_message
self.model_name = model_name
self.step_counter = 0
self.traces = []
self.logdir = logdir
# Create log directory if it doesn't exist
os.makedirs(self.logdir, exist_ok=True)
# Define available tools
self.tools = [
{
"type": "function",
"function": {
"name": "add",
"description": "Add two numbers together",
"parameters": {
"type": "object",
"properties": {
"a": {"type": "number", "description": "First number"},
"b": {"type": "number", "description": "Second number"},
},
"required": ["a", "b"],
},
},
},
{
"type": "function",
"function": {
"name": "sub",
"description": "Subtract second number from first number",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number",
"description": "Number to subtract from",
},
"b": {
"type": "number",
"description": "Number to subtract",
},
},
"required": ["a", "b"],
},
},
},
{
"type": "function",
"function": {
"name": "mul",
"description": "Multiply two numbers together",
"parameters": {
"type": "object",
"properties": {
"a": {"type": "number", "description": "First number"},
"b": {"type": "number", "description": "Second number"},
},
"required": ["a", "b"],
},
},
},
{
"type": "function",
"function": {
"name": "div",
"description": "Divide first number by second number",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number",
"description": "Number to divide (numerator)",
},
"b": {
"type": "number",
"description": "Number to divide by (denominator)",
},
},
"required": ["a", "b"],
},
},
},
]
def add(self, a: float, b: float) -> float:
"""Add two numbers"""
result = a + b
return result
def sub(self, a: float, b: float) -> float:
"""Subtract b from a"""
result = a - b
return result
def mul(self, a: float, b: float) -> float:
"""Multiply two numbers"""
result = a * b
return result
def div(self, a: float, b: float) -> float:
"""Divide a by b"""
if b == 0:
raise ValueError("Division by zero")
result = a / b
return result
def _execute_tool_call(self, tool_call) -> str:
"""Execute a tool call and return the result"""
self.traces.append(
TraceEvent(
event_type="tool_execution",
component="math_tools",
data={
"tool_name": tool_call.function.name,
"args": json.loads(tool_call.function.arguments),
},
)
)
function_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
# Execute the appropriate function
if function_name == "add":
result = self.add(arguments["a"], arguments["b"])
elif function_name == "sub":
result = self.sub(arguments["a"], arguments["b"])
elif function_name == "mul":
result = self.mul(arguments["a"], arguments["b"])
elif function_name == "div":
result = self.div(arguments["a"], arguments["b"])
else:
raise ValueError(f"Unknown function: {function_name}")
self.traces.append(
TraceEvent(
event_type="tool_result",
component="math_tools",
data={
"result": result,
},
)
)
return str(result)
def export_traces_to_log(
self, run_id: str, problem: str, final_result: Optional[float] = None
):
"""
Export traces to a log file with run_id
Args:
run_id: Unique identifier for this run
problem: The problem that was solved
final_result: The final result of the computation
"""
timestamp = datetime.now().isoformat()
log_filename = (
f"run_{run_id}_{timestamp.replace(':', '-').replace('.', '-')}.json"
)
log_filepath = os.path.join(self.logdir, log_filename)
log_data = {
"run_id": run_id,
"timestamp": timestamp,
"problem": problem,
"final_result": final_result,
"model_name": self.model_name,
"traces": [asdict(trace) for trace in self.traces],
}
with open(log_filepath, "w") as f:
json.dump(log_data, f, indent=2)
logging.info(f"Traces exported to: {log_filepath}")
return log_filepath
def solve(
self, problem: str, max_iterations: int = 10, run_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Solve a math problem using iterative planning with LLM and atomic tools
Args:
problem: Mathematical expression or problem to solve
max_iterations: Maximum number of LLM iterations to prevent infinite loops
run_id: Optional run identifier. If None, generates one automatically
Returns:
Final numerical result
"""
# Generate run_id if not provided
if run_id is None:
run_id = f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{hash(problem) % 10000:04d}"
# Reset traces for each new problem
self.traces = []
logging.info(f"Solving: {problem} (Run ID: {run_id})")
logging.info("=" * 60)
# Reset state
self.execution_history = []
self.step_counter = 0
messages = [
{"role": "system", "content": self.system_message},
{
"role": "user",
"content": f"Solve this mathematical expression step by step: {problem}",
},
]
iteration = 0
while iteration < max_iterations:
iteration += 1
logging.info(f"\n--- LLM Iteration {iteration} ---")
try:
self.traces.append(
TraceEvent(
event_type="llm_call",
component="openai_api",
data={
"model": self.model_name,
"messages": messages,
# "tools": [tool["function"] for tool in self.tools]
},
)
)
# Call OpenAI API with function calling
response = self.client.chat.completions.create(
model=self.model_name,
messages=messages,
tools=self.tools,
tool_choice="auto",
# temperature=0
)
message = response.choices[0].message
messages.append(message.model_dump())
self.traces.append(
TraceEvent(
event_type="llm_response",
component="openai_api",
data={
"content": message.content,
"tool_calls": (
[tool.model_dump() for tool in message.tool_calls]
if message.tool_calls
else []
),
},
)
)
# Check if the model wants to call functions
if message.tool_calls:
logging.info(
f"LLM planning: {message.content or 'Executing tools...'}"
)
# Execute each tool call
for tool_call in message.tool_calls:
result = self._execute_tool_call(tool_call)
# Add tool result to conversation
messages.append(
{
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
}
)
else:
# No more tool calls - this should be the final answer
logging.info(f"LLM final response: {message.content}")
# Try to extract the numerical result
try:
# Look for a number in the response
import re
numbers = re.findall(r"-?\d+\.?\d*", message.content)
if numbers:
final_result = float(
numbers[-1]
) # Take the last number found
logging.info("=" * 60)
logging.info(f"Final result: {final_result}")
self.traces.append(
TraceEvent(
event_type="result_extraction",
component="math_tools",
data={"final_result": final_result},
)
)
# Export traces to log file
log_filename = self.export_traces_to_log(
run_id, problem, final_result
)
return {"result": final_result, "log_file": log_filename}
else:
logging.info(
"Could not extract numerical result from LLM response"
)
break
except ValueError:
logging.info("Could not parse final result as number")
break
except Exception as e:
logging.info(f"Error in iteration {iteration}: {e}")
break
logging.info("Max iterations reached or error occurred")
# Export traces even if solve failed
return {
"result": 0,
"log_file": self.export_traces_to_log(run_id, problem, 0.0),
}
def get_default_agent(
model_name: str = "gpt-4o", logdir: str = "logs"
) -> MathToolsAgent:
"""Get a default instance of the MathToolsAgent with OpenAI client"""
openai_client = openai.OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
return MathToolsAgent(client=openai_client, model_name=model_name, logdir=logdir)
if __name__ == "__main__":
# Example usage
client = openai.OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
agent = MathToolsAgent(client, logdir="agent_logs")
problem = "((2 + 3) * 4) - (6 / 2)"
print(f"Problem: {problem}")
result = agent.solve(problem)
print(f"Result: {result}")