import json import os import re import sys import dotenv import requests from config_raw_fc_tg import config # ------------------------------------------------------------------ # CONFIGURATION LOAD # ------------------------------------------------------------------ # 1. Load .env (for security, environment variables are preferred for keys) dotenv.load_dotenv() # 3. Resolve Constants # Logic: Try Environment Variable first -> Then Config File -> Then Error TOGETHER_API_KEY = os.getenv("TOGETHER_API_KEY") or config.get("together_api_key") if not TOGETHER_API_KEY or TOGETHER_API_KEY == "YOUR_TOGETHER_API_KEY_HERE": print( "❌ [ERROR] Missing TOGETHER_API_KEY. Set it in .env or raw_function_call_test_config.json" ) sys.exit(1) URL = config.get("url", "https://api.together.xyz/v1/chat/completions") MODEL = config.get("model", "moonshotai/Kimi-K2.5") TEST_PROMPT = config.get("test_prompt", "Please fetch me the flight times...") print(f"⚙️ [CONFIG] Model: {MODEL}") print(f"⚙️ [CONFIG] Prompt: {TEST_PROMPT[:50]}...") # ------------------------------------------------------------------ # 1. Tool Definitions # ------------------------------------------------------------------ flight_func_schema = { "type": "function", "function": { "name": "get_flight_times", "description": "Return flight times between two airport codes.", "parameters": { "type": "object", "properties": { "departure": {"type": "string"}, "arrival": {"type": "string"}, }, "required": ["departure", "arrival"], }, }, } def execute_local_tool(tool_name: str, arguments) -> str: """Executes the specific local function logic.""" print(f"\n[LOCAL EXECUTION] Running {tool_name} with args: {arguments}") if tool_name == "get_flight_times": dep = arguments.get("departure") arr = arguments.get("arrival") return json.dumps( { "status": "success", "departure": dep, "arrival": arr, "duration": "4h 30m", "departure_time": "10:30 AM PST", "arrival_time": "06:30 PM EST", } ) return json.dumps({"error": f"Tool {tool_name} not found"}) # ------------------------------------------------------------------ # 2. Parsing Logic (The "Dual" Detector) # ------------------------------------------------------------------ def parse_hermes_style(content: str): """ Attempts to extract function calls from text content using common XML or JSON patterns used by Hermes/Llama-3 finetunes. """ tools_found = [] # Pattern 1: {JSON} (Hyperbolic/Hermes specific) fc_matches = re.findall(r"(.*?)", content, re.DOTALL) # Pattern 2: {JSON} (Hermes generic) tc_matches = re.findall(r"(.*?)", content, re.DOTALL) # Combine matches all_matches = fc_matches + tc_matches for raw_json in all_matches: try: # Cleanup common formatting issues cleaned_json = raw_json.strip() # Handle "name": "foo", "arguments": ... structure data = json.loads(cleaned_json) # If it's the {name, arguments} wrapper if "name" in data and "arguments" in data: tools_found.append( { "type": "hermes_wrapper", "name": data["name"], "arguments": data["arguments"], } ) # If it's just the arguments (rare, but happens if name is implied) else: # We assume the name was mentioned prior, strictly this is harder to handle generically # skipping for now unless explicit structure found pass except json.JSONDecodeError: # Fallback: Regex extraction for FunctionName { args } func_match = re.match( r"^\s*([a-zA-Z0-9_]+)\s*(\{.*)", cleaned_json, re.DOTALL ) if func_match: try: name = func_match.group(1) args = json.loads(func_match.group(2)) tools_found.append( {"type": "hermes_text", "name": name, "arguments": args} ) except Exception: pass return tools_found # ------------------------------------------------------------------ # 3. Stream Handler # ------------------------------------------------------------------ def run_turn(messages): headers = { "Content-Type": "application/json", "Authorization": f"Bearer {TOGETHER_API_KEY}", } # We send the tools definition. # Even for Hermes style, the model often needs to 'see' the tools in the schema # to know they exist, even if it decides to text-complete the call. payload = { "model": MODEL, "messages": messages, "tools": [flight_func_schema], "tool_choice": "auto", "max_tokens": 1024, "temperature": 0.4, "stream": True, } print(f"\n=== 🔍 VISUAL DEBUG: OUTBOUND PAYLOAD (Length: {len(messages)}) ===") # Print last message for brevity print(json.dumps(messages[-1], indent=2)) print("===================================================================\n") print(f"--- STARTING STREAM TURN (Msgs: {len(messages)}) ---") # Accumulators full_content = "" tool_calls_buffer = {} # Protocol Detection detected_protocol = "none" # 'native', 'hermes', 'none' with requests.post(URL, headers=headers, json=payload, stream=True) as response: if response.status_code != 200: print(f"\n[CRITICAL ERROR] Status Code: {response.status_code}") print(f"Raw Response: {response.text}") response.raise_for_status() for line in response.iter_lines(): if not line: continue decoded = line.decode("utf-8") if not decoded.startswith("data: "): continue data_str = decoded[6:].strip() if data_str == "[DONE]": print("\n[DEBUG STREAM] Received [DONE]") break try: chunk = json.loads(data_str) choices = chunk.get("choices", []) if not choices: continue delta = choices[0].get("delta", {}) # -------------------------------------------------------- # 1. NATIVE DETECTION # -------------------------------------------------------- if delta.get("tool_calls"): detected_protocol = "native" for tc in delta["tool_calls"]: idx = tc["index"] if idx not in tool_calls_buffer: tool_calls_buffer[idx] = { "id": tc.get("id", ""), "function": {"name": "", "arguments": ""}, } fn = tc.get("function", {}) if fn.get("name"): tool_calls_buffer[idx]["function"]["name"] += fn["name"] if fn.get("arguments"): tool_calls_buffer[idx]["function"]["arguments"] += fn[ "arguments" ] # Visual feedback for Native sys.stdout.write("N") sys.stdout.flush() # -------------------------------------------------------- # 2. CONTENT ACCUMULATION (Potential Hermes) # -------------------------------------------------------- content = delta.get("content") if content: full_content += content # Visual feedback for Content sys.stdout.write(".") sys.stdout.flush() except json.JSONDecodeError: pass print("\n--- STREAM FINISHED ---") # Construct the final message object message_obj = {"role": "assistant", "content": full_content if full_content else ""} parsed_hermes_tools = [] # ------------------------------------------------------------------ # POST-PROCESSING: DETERMINE WINNER # ------------------------------------------------------------------ # Priority 1: Native if tool_calls_buffer: detected_protocol = "native" formatted_tools = [] for idx in sorted(tool_calls_buffer.keys()): tool_data = tool_calls_buffer[idx] # Ensure ID exists t_id = ( tool_data["id"] if tool_data["id"] else f"call_{idx}_{hash(tool_data['function']['arguments'])}" ) formatted_tools.append( { "id": t_id, "type": "function", "function": { "name": tool_data["function"]["name"], "arguments": tool_data["function"]["arguments"], }, } ) message_obj["tool_calls"] = formatted_tools # Priority 2: Hermes/Text (If no native calls) else: # Check for XML tags in content parsed_hermes_tools = parse_hermes_style(full_content) if parsed_hermes_tools: detected_protocol = "hermes" # We treat these as "virtual" tool calls for the script logic, # but we DO NOT add "tool_calls" to the message_obj because # the API expects "tool_calls" to map to "role: tool". # For Hermes, the tool call is IN the content. return message_obj, detected_protocol, parsed_hermes_tools # ------------------------------------------------------------------ # 3. Main Logic # ------------------------------------------------------------------ if __name__ == "__main__": conversation_history = [{"role": "user", "content": TEST_PROMPT}] # --- TURN 1 --- assistant_msg, protocol, hermes_tools = run_turn(conversation_history) conversation_history.append(assistant_msg) print(f"\n📊 [INTERMEDIATE REPORT] Protocol Detected: {protocol.upper()}") tools_to_execute = [] # A. Handle Native if protocol == "native": print( f"[SYSTEM] Native Tool Calls Detected: {len(assistant_msg['tool_calls'])}" ) for tc in assistant_msg["tool_calls"]: tools_to_execute.append( { "id": tc["id"], "name": tc["function"]["name"], "args_str": tc["function"]["arguments"], "type": "native", } ) # B. Handle Hermes elif protocol == "hermes": print(f"[SYSTEM] Hermes/Text Tool Calls Detected: {len(hermes_tools)}") for i, ht in enumerate(hermes_tools): # Generate a fake ID for tracking tools_to_execute.append( { "id": f"hermes_call_{i}", "name": ht["name"], "args_str": ( json.dumps(ht["arguments"]) if isinstance(ht["arguments"], dict) else ht["arguments"] ), "type": "hermes", } ) else: print("[SYSTEM] No tool calls detected.") print(f"[RESPONSE] {assistant_msg.get('content')}") sys.exit(0) # --- EXECUTION LOOP --- if tools_to_execute: print(f"[SYSTEM] Executing {len(tools_to_execute)} tool(s)...") for t in tools_to_execute: # Parse Args try: func_args = json.loads(t["args_str"]) except Exception: print(f"[ERROR] Failed to parse args: {t['args_str']}") func_args = {} # Execute result_str = execute_local_tool(t["name"], func_args) # --- CRITICAL: INJECT RESPONSE BASED ON PROTOCOL --- if t["type"] == "native": # Standard OpenAI Format conversation_history.append( { "role": "tool", "tool_call_id": t["id"], "name": t["name"], "content": result_str, } ) print(" -> Appended [Native] 'tool' role response.") elif t["type"] == "hermes": # Hermes Style: JSON # Usually sent as "role": "tool" (newer) or "role": "user" (classic Hermes) # We will use "role": "tool" with the XML wrapper as it is the most robust hybrid approach. hermes_response_content = ( f"\n{result_str}\n" ) conversation_history.append( { "role": "tool", "name": t["name"], "content": hermes_response_content, } ) print(" -> Appended [Hermes] XML response wrapper.") # --- TURN 2 (The Reaction) --- print("\n[SYSTEM] Sending tool outputs back to model...") final_msg, final_proto, _ = run_turn(conversation_history) print("\n===================================================================") print("🎉 FINAL ANALYSIS REPORT") print("===================================================================") print(f"1. Model: {MODEL}") print(f"2. Preferred Protocol: {protocol.upper()}") print(f"3. Tool Execution Successful: {'Yes' if tools_to_execute else 'No'}") print(f"4. Final Answer:\n{final_msg.get('content')}") print("===================================================================")