import re import json import datetime import os from typing import List, Dict, Any, Callable from langchain_core.messages import SystemMessage, HumanMessage, BaseMessage from langchain.tools import tool from langchain_ollama import ChatOllama from langchain_core.tools import render_text_description # --- 1. Tool Definitions --- @tool def validate_user(user_id: int, addresses: List[str]) -> bool: """Validate user using historical addresses. Args: user_id (int): the user ID. addresses (List[str]): Previous addresses as a list of strings. """ # In a real scenario, this would involve some logic. # For now, it just returns True as in the original script. return True def get_tools() -> List[Callable]: """Returns a list of available tools.""" return [validate_user] # --- 2. Prompt Engineering --- def create_system_prompt(tools: List[Callable]) -> str: """Creates a system prompt with tool descriptions.""" tool_description = render_text_description(tools) # Using triple quotes for the f-string to handle quotes inside easily. return f'''# Tools You may call one or more functions to assist with the user query. You are provided with function signatures within XML tags: {tool_description} For each function call, first think about what you need to do and write it down in a block. Then, return a JSON object with the function name and arguments within XML tags. **IMPORTANT**: The JSON object must be a valid JSON, with all keys and string values enclosed in double quotes. Example of a valid tool call: {{"name": "validate_user", "arguments": {{"user_id": 123, "addresses": ["123 Fake St", "456 Main St"]}}}} ''' # --- 3. LLM Response Parsing --- def parse_llm_response(content: str) -> Dict[str, Any]: """Parses the LLM's response to extract reasoning and tool calls.""" think_match = re.search(r"(.*?)", content, re.DOTALL) reasoning = think_match.group(1).strip() if think_match else None tool_call_match = re.search(r"(.*?)", content, re.DOTALL) tool_call_str = tool_call_match.group(1).strip() if tool_call_match else None tool_call = None parsing_error = None if tool_call_str: try: # The model sometimes returns a markdown code block. if tool_call_str.startswith("```json"): tool_call_str = tool_call_str[7:-4] tool_call = json.loads(tool_call_str) except json.JSONDecodeError as e: parsing_error = f"Failed to parse tool_call JSON: {e}" return { "reasoning": reasoning, "tool_call": tool_call, "parsing_error": parsing_error, } # --- 4. Agent Core Logic --- def execute_tool_call(tool_call: Dict[str, Any], tools: List[Callable]) -> Dict[str, Any]: """Executes a tool call and returns the result.""" if not tool_call: return {"error": "No tool call provided."} tool_name = tool_call.get("name") tool_args = tool_call.get("arguments") tool_to_call = next((t for t in tools if t.name == tool_name), None) if not tool_to_call: return {"error": f"Tool '{tool_name}' not found."} try: result = tool_to_call.invoke(tool_args) return {"result": result} except Exception as e: return {"error": f"Error calling tool '{tool_name}': {e}"} def save_log(log_data: Dict[str, Any]): """Saves the log data to a JSON file.""" # Get the directory of the current script script_dir = os.path.dirname(os.path.abspath(__file__)) # Go up three levels to the project root and then into the `output` directory output_dir = os.path.join(script_dir, "..", "..", "..", "output") os.makedirs(output_dir, exist_ok=True) timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f") # get the script name script_name = os.path.basename(__file__).split('.')[0] file_path = os.path.join(output_dir, f"{script_name}_{timestamp}.json") with open(file_path, "w") as f: json.dump(log_data, f, indent=2) print(f"Log saved to {os.path.normpath(file_path)}") def main(): """Main function to run the agent.""" # --- Configuration --- model_config = { "model": "qwen3:1.7b", "temperature": 0, } human_message_content = "Could you validate user 123? They previously lived at 123 Fake St in Boston MA and 234 Pretend Boulevard in Houston TX. Think before you call any tools" # --- Initialization --- log_data = {"run_config": {"model_config": model_config}} tools = get_tools() llm = ChatOllama(**model_config) system_prompt = create_system_prompt(tools) messages: List[BaseMessage] = [ SystemMessage(content=system_prompt), HumanMessage(content=human_message_content), ] # --- Log initial data --- log_data["messages"] = [json.loads(msg.model_dump_json()) for msg in messages] # --- LLM Invocation --- response = llm.invoke(messages) log_data["raw_response"] = json.loads(response.model_dump_json()) # --- Parsing --- parsed_response = parse_llm_response(response.content) log_data.update(parsed_response) # --- Tool Execution --- tool_result = None if parsed_response["tool_call"]: tool_result = execute_tool_call(parsed_response["tool_call"], tools) log_data["tool_result"] = tool_result # --- Print to console (to replicate original behavior) --- print("--- Extracted Reasoning ---") print(log_data.get("reasoning")) print("\n" + "="*40 + "\n") print("--- Extracted Tool Call ---") print(log_data.get("tool_call")) if tool_result: print("\n" + "="*40 + "\n") if "result" in tool_result: print(f"--- Calling Tool: {log_data['tool_call']['name']} ---") print("--- Tool Call Result ---") print(tool_result["result"]) else: print("--- Tool Call Error ---") print(tool_result["error"]) print("\n" + "="*40 + "\n") # --- Save comprehensive log --- save_log(log_data) if __name__ == "__main__": main()