File size: 9,779 Bytes
434c049
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
#!/usr/bin/env python3
"""
scripts/omniroute_freight_synthesizer.py — OmniRoute 'paid-premium' Multi-Agent Freight Dataset Synthesizer.

Generates hyper-realistic multi-turn freight negotiation training dialogues using OmniRoute's
high-reasoning 'paid-premium' combo model.

Output complies with:
- OpenAI tool-calling chat completions format
- LoadETA tool schemas (fmcsa_verify_mc, calculate_rate_floor, get_truck_route_distance, book_load_offer)
- LiveKit voice conversational requirements (phonetic currencies, spoken cadence, no markdown)

Usage:
    python3 scripts/omniroute_freight_synthesizer.py --count 10 --output data/freight_negotiation_omniroute.jsonl
"""

import os
import sys
import json
import random
import time
import argparse
import urllib.request
import urllib.error
from typing import Dict, Any, List

OMNIROUTE_URL = "http://100.70.158.21:20128/v1/chat/completions"
OMNIROUTE_MODEL = "paid-premium"

SEED_SCENARIOS = [
    {
        "lane": "Atlanta, GA to Chicago, IL",
        "miles": 715,
        "equipment": "53ft Reefer",
        "temp": "-10F continuous",
        "commodity": "Frozen Poultry",
        "weight": "43,000 lbs",
        "broker_persona": "Aggressive lowball broker trying to anchor rate at $1,800 on a $2,400 lane",
        "special_condition": "Requires strict 2 hours free detention agreement and temp recorder verification",
    },
    {
        "lane": "Laredo, TX to Dallas, TX",
        "miles": 430,
        "equipment": "53ft Dry Van",
        "temp": "ambient",
        "commodity": "Cross-border Automotive Parts",
        "weight": "42,500 lbs",
        "broker_persona": "Urgent hot-load broker with auto assembly line shutdown risk if not picked up in 90 mins",
        "special_condition": "High rate elasticity, dispatcher pushes for premium rate ($1,650)",
    },
    {
        "lane": "Allentown, PA to Richmond, VA",
        "miles": 260,
        "equipment": "48ft Flatbed",
        "temp": "ambient",
        "commodity": "Structural Steel & Beams with 8ft Tarp",
        "weight": "46,000 lbs",
        "broker_persona": "Mid-tier broker trying to avoid paying $150 tarp fee",
        "special_condition": "Dispatcher stands firm on tarping surcharge and Northeast toll reimbursement",
    },
    {
        "lane": "Los Angeles, CA to Phoenix, AZ",
        "miles": 375,
        "equipment": "53ft Reefer",
        "temp": "+34F pre-cooled",
        "commodity": "Fresh Organic Berries",
        "weight": "38,000 lbs",
        "broker_persona": "Double-broker suspect with newly registered MC (42 days old)",
        "special_condition": "FMCSA tool flags new authority and lack of credit score; dispatcher politely refuses load",
    },
    {
        "lane": "Houston, TX to Savannah, GA",
        "miles": 840,
        "equipment": "53ft Dry Van",
        "temp": "ambient",
        "commodity": "Retail Consumer Goods",
        "weight": "36,000 lbs",
        "broker_persona": "Standard C.H. Robinson corporate broker negotiating multi-stop delivery",
        "special_condition": "2 pick-up locations and 2 drop-offs; dispatcher negotiates $100 extra per stop",
    },
]


def get_omniroute_api_key() -> str:
    key = os.environ.get("OMNIROUTE_API_KEY")
    if key:
        return key
    secrets_path = os.path.expanduser("~/.mcp/secrets/omniroute.env")
    if os.path.exists(secrets_path):
        with open(secrets_path, "r", encoding="utf-8") as f:
            for line in f:
                if line.startswith("OMNIROUTE_MCP_KEY="):
                    return line.strip().split("=", 1)[1]
    return ""


def call_omniroute(prompt: str, system: str = "") -> str:
    key = get_omniroute_api_key()
    headers = {
        "Authorization": f"Bearer {key}",
        "Content-Type": "application/json",
    }
    payload = {
        "model": OMNIROUTE_MODEL,
        "messages": [
            {"role": "system", "content": system or "You are an expert synthetic dataset engineer for autonomous freight negotiation voice agents."},
            {"role": "user", "content": prompt},
        ],
        "temperature": 0.7,
        "max_tokens": 2500,
    }

    req = urllib.request.Request(
        OMNIROUTE_URL,
        data=json.dumps(payload).encode("utf-8"),
        headers=headers,
    )
    with urllib.request.urlopen(req, timeout=120) as resp:
        res = json.loads(resp.read().decode("utf-8"))
        return res["choices"][0]["message"]["content"]


def extract_json(raw_text: str) -> Dict[str, Any]:
    import re
    # 1. Try markdown code blocks first (often the cleanest JSON)
    blocks = re.findall(r"```(?:json)?\s*([\s\S]*?)\s*```", raw_text)
    for block in reversed(blocks):
        try:
            val = json.loads(block.strip())
            if isinstance(val, dict) and ("messages" in val or "turns" in val or "id" in val):
                return val
        except Exception:
            pass

    for block in blocks:
        try:
            val = json.loads(block.strip())
            if isinstance(val, dict):
                return val
        except Exception:
            pass

    # 2. Try whole string directly
    try:
        return json.loads(raw_text.strip())
    except Exception:
        pass

    # 3. Scan from first { to last }
    s = raw_text.find("{")
    e = raw_text.rfind("}")
    if s != -1 and e != -1 and e > s:
        try:
            return json.loads(raw_text[s:e+1])
        except Exception:
            pass

    raise ValueError(f"No valid JSON object could be extracted from: {raw_text[:120]}")


def synthesize_conversation(scenario: Dict[str, Any], index: int) -> Dict[str, Any]:
    prompt = f"""
Generate an authentic, complete multi-turn freight negotiation training conversation between a US Freight Broker and LoadETA (an autonomous dispatcher voice agent).

Scenario Parameters:
- Lane: {scenario['lane']} ({scenario['miles']} miles)
- Equipment: {scenario['equipment']}
- Commodity & Weight: {scenario['commodity']} ({scenario['weight']})
- Temperature: {scenario['temp']}
- Broker Profile: {scenario['broker_persona']}
- Special Case: {scenario['special_condition']}

Strict Rules for LoadETA Spoken Output:
1. Format exclusively for human voice synthesis (LiveKit TTS): No markdown bolding, no bullet points, no emojis, no robotic phrasing.
2. Spoken numbers and currencies: use natural spoken words for money (e.g. "twenty-four hundred", "eighteen fifty all in", "seventy-five dollars an hour").
3. Tool Calling: Assistant MUST include tool calls with exact arguments:
   - 'fmcsa_verify_mc' with arguments {{"mc_number": "...", "broker_name": "..."}}
   - 'calculate_rate_floor' with arguments {{"origin": "...", "destination": "...", "equipment_type": "...", "mileage": {scenario['miles']}, "weight": 42000}}
   - 'book_load_offer' with arguments {{"broker_name": "...", "mc_number": "...", "rate_agreed": 2400, "detention_rate_per_hr": 75}}

You must output a JSON object with this exact key structure:
{{
  "id": "omniroute_freight_{index:05d}",
  "messages": [
    {{"role": "system", "content": "You are LoadETA, an expert freight dispatcher and live negotiation voice agent for US commercial trucking. Protect driver margins, verify broker authority, and negotiate rates naturally for voice without markdown."}},
    {{"role": "user", "content": "Broker opening pitch..."}},
    {{"role": "assistant", "content": null, "tool_calls": [{{"id": "call_fmcsa_{index:04d}", "type": "function", "function": {{"name": "fmcsa_verify_mc", "arguments": "{{\\"mc_number\\": \\"782914\\", \\"broker_name\\": \\"Apex Logistics\\"}}"}}}}]}},
    {{"role": "tool", "tool_call_id": "call_fmcsa_{index:04d}", "content": "{{\\"status\\": \\"CLEAN\\", \\"active\\": true, \\"credit_score\\": 94}}"}}
    {{"role": "assistant", "content": "Natural counter-offer in spoken voice..."}},
    {{"role": "user", "content": "Broker counter..."}},
    {{"role": "assistant", "content": null, "tool_calls": [{{"id": "call_book_{index:04d}", "type": "function", "function": {{"name": "book_load_offer", "arguments": "{{\\"rate_agreed\\": 2400}}"}}}}]}},
    {{"role": "tool", "tool_call_id": "call_book_{index:04d}", "content": "{{\\"status\\": \\"BOOKED\\"}}"}},
    {{"role": "assistant", "content": "Final confirmation..."}}
  ]
}}

OUTPUT JSON ONLY. Do not write explanations or notes.
"""

    raw_response = call_omniroute(prompt)
    try:
        data = extract_json(raw_response)
        return data
    except Exception as e:
        print(f"JSON decode failed for scenario {index}: {e}. Raw head: {repr(raw_response[:100])}")
        return None


def main():
    parser = argparse.ArgumentParser(description="OmniRoute Paid-Premium Freight Dataset Synthesizer")
    parser.add_argument("--count", type=int, default=5, help="Number of dialogues to synthesize")
    parser.add_argument("--output", default="freight/data/freight_negotiation_omniroute.jsonl", help="Output JSONL file")
    args = parser.parse_args()

    os.makedirs(os.path.dirname(args.output), exist_ok=True)
    print(f"Connecting to OmniRoute ({OMNIROUTE_URL}) with model '{OMNIROUTE_MODEL}'...")

    results = []
    for i in range(1, args.count + 1):
        scenario = random.choice(SEED_SCENARIOS)
        print(f"[{i}/{args.count}] Synthesizing: {scenario['lane']} ({scenario['equipment']})...")
        conv = synthesize_conversation(scenario, i)
        if conv:
            results.append(conv)
            with open(args.output, "a" if i > 1 else "w", encoding="utf-8") as f:
                f.write(json.dumps(conv) + "\n")
            print(f"  -> Generated {len(conv.get('messages', []))} turns.")
        time.sleep(1)

    print(f"\nSuccessfully generated {len(results)} high-reasoning dialogues in {args.output}")


if __name__ == "__main__":
    main()