#!/usr/bin/env python3 from __future__ import annotations import argparse import hashlib import json import re import sqlite3 import time from pathlib import Path from typing import Any from urllib import request REPO_ROOT = Path(__file__).resolve().parents[1] DEFAULT_ACTUALS = [ REPO_ROOT / "data/evals/ody_search_teacher_pipeline_20260821/deepseek_actual/actual_results.json", REPO_ROOT / "data/evals/ody_v57_quick_live_search_cases_20260821/v59_run_20260821_2042/actual_results.json", ] DEFAULT_OUT_DIR = Path("/home/pewds/odysseus-finetune/data/teacher_live_gaps/odysseus_v60_realistic_verbose_web_synthesis_20260821") WEB_TOOLS = {"web_search", "web_fetch"} FORBIDDEN_FINAL_RE = re.compile( r"WEB SEARCH RESULTS|```sources|\b\d+\s+Web sources\b|from the search results|results indicate|returned snippets|top results|i searched", re.IGNORECASE, ) TOOL_SCHEMAS = [ { "type": "function", "function": { "name": "web_search", "description": "Search the public web for source-backed information.", "parameters": { "type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"], }, }, }, { "type": "function", "function": { "name": "web_fetch", "description": "Fetch a specific URL when search snippets do not contain enough evidence.", "parameters": { "type": "object", "properties": {"url": {"type": "string"}}, "required": ["url"], }, }, }, ] def stable_id(prefix: str, obj: dict[str, Any]) -> str: payload = json.dumps(obj, sort_keys=True, ensure_ascii=True) return prefix + "_" + hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16] def load_teacher_endpoint(db_path: Path, model: str | None) -> dict[str, str]: conn = sqlite3.connect(db_path) conn.row_factory = sqlite3.Row try: row = conn.execute( """ SELECT base_url, api_key, cached_models FROM model_endpoints WHERE is_enabled = 1 AND api_key IS NOT NULL AND api_key != '' AND (lower(name) LIKE '%deepseek%' OR lower(id) LIKE '%deepseek%') ORDER BY updated_at DESC LIMIT 1 """ ).fetchone() finally: conn.close() if row is None: raise RuntimeError("no enabled DeepSeek endpoint with API key found in app DB") selected_model = model if not selected_model: cached = json.loads(row["cached_models"] or "[]") selected_model = cached[0] if cached else "deepseek-v4-flash" return {"base_url": row["base_url"], "api_key": row["api_key"], "model": selected_model} def call_json(endpoint: dict[str, str], payload: dict[str, Any]) -> dict[str, Any]: body = { "model": endpoint["model"], "messages": [ { "role": "system", "content": ( "Return strict JSON only. You are creating SFT final answers for web tool traces. " "Do not include chain-of-thought or prose outside JSON." ), }, {"role": "user", "content": json.dumps(payload, ensure_ascii=False)}, ], "temperature": 0.2, "max_tokens": 900, "response_format": {"type": "json_object"}, } req = request.Request( endpoint["base_url"].rstrip("/") + "/chat/completions", data=json.dumps(body).encode("utf-8"), headers={"Content-Type": "application/json", "Authorization": f"Bearer {endpoint['api_key']}"}, method="POST", ) with request.urlopen(req, timeout=180) as resp: parsed = json.loads(resp.read().decode("utf-8")) text = str(parsed["choices"][0]["message"].get("content") or "").strip() text = re.sub(r"^```(?:json)?\s*|\s*```$", "", text, flags=re.IGNORECASE | re.DOTALL).strip() return json.loads(text) def normalize_args(tool: str, args: Any) -> dict[str, Any]: if isinstance(args, dict): return args if isinstance(args, str): stripped = args.strip() if stripped.startswith("{"): try: parsed = json.loads(stripped) if isinstance(parsed, dict): return parsed except json.JSONDecodeError: pass return {"query": stripped} if tool == "web_search" else {"url": stripped} return {} def compact_tool_output(text: str, max_chars: int = 3000) -> str: text = re.sub(r"\r\n?", "\n", text or "").strip() text = re.sub(r"\n{3,}", "\n\n", text) if len(text) <= max_chars: return text sources = "" if text.startswith("```sources"): end = text.find("```", 3) if end != -1: sources = text[: end + 3].strip() summary_match = re.search(r"SEARCH RESULTS SUMMARY:\n[-]+\n(?P.*?)(?:\n={10,}|\Z)", text, re.DOTALL) summary = summary_match.group("body").strip() if summary_match else "" fetched_match = re.search(r"FETCHED PAGE CONTENT:\n[-]+\n(?P.*?)(?:\n={10,}|\Z)", text, re.DOTALL) fetched = fetched_match.group("body").strip() if fetched_match else "" chunks = [chunk for chunk in [sources, summary[:1600], fetched[:900]] if chunk] compact = "\n\n".join(chunks).strip() if not compact: compact = text[:max_chars].rstrip() return compact[:max_chars].rstrip() def load_results(paths: list[Path]) -> list[dict[str, Any]]: out: list[dict[str, Any]] = [] seen: set[str] = set() for path in paths: payload = json.loads(path.read_text(encoding="utf-8")) for result in payload.get("results") or []: key = f"{path}:{result.get('id')}" if key in seen: continue seen.add(key) result = dict(result) result["_source_path"] = str(path) out.append(result) return out def load_teacher_finals(path: Path | None) -> dict[str, str]: if path is None: return {} payload = json.loads(path.read_text(encoding="utf-8")) finals: dict[str, str] = {} for item in payload.get("edits") or []: if not item.get("accepted"): continue edited = item.get("edited") or {} final = str(edited.get("final") or "").strip() if final and not FORBIDDEN_FINAL_RE.search(final): finals[str(item.get("id"))] = final return finals def usable_web_steps(result: dict[str, Any], max_tools: int) -> list[dict[str, Any]]: calls = result.get("tool_calls") or [] outputs = result.get("tool_outputs") or [] steps: list[dict[str, Any]] = [] for idx, call in enumerate(calls): tool = call.get("tool") or call.get("name") if tool not in WEB_TOOLS: continue if idx >= len(outputs): continue output = outputs[idx] if output.get("tool") and output.get("tool") not in WEB_TOOLS: continue args = normalize_args(tool, call.get("args")) if tool == "web_search" and not args.get("query"): continue if tool == "web_fetch" and not args.get("url"): continue content = compact_tool_output(str(output.get("output") or "")) if not content: continue steps.append({"tool": tool, "args": args, "output": content}) if len(steps) >= max_tools: break return steps def teacher_final(endpoint: dict[str, str], result: dict[str, Any], steps: list[dict[str, Any]]) -> dict[str, Any]: prompt = { "task": "Write the assistant's final answer after these web tool calls.", "current_date": "2026-08-21", "user": result.get("user") or "", "prior_turns": result.get("prior_turns") or [], "tool_steps": steps, "bad_actual_final": result.get("final_answer") or "", "requirements": [ "Return JSON with should_train boolean, final string, and reason string.", "Use the tool evidence to answer the user's actual question directly.", "If snippets are insufficient for a precise value, say the best supported answer and the uncertainty briefly.", "Do not say 'from the search results', 'results indicate', 'snippets', 'I searched', or list sources.", "Do not copy raw snippets. Synthesize.", "Keep the final to 1-4 short sentences.", "If this request should not have searched, set should_train=false.", ], } return call_json(endpoint, prompt) def build_row(result: dict[str, Any], steps: list[dict[str, Any]], final: str) -> dict[str, Any] | None: final = re.sub(r"\s+", " ", final).strip() if not final or len(final) > 900 or FORBIDDEN_FINAL_RE.search(final): return None messages: list[dict[str, Any]] = [] for turn in result.get("prior_turns") or []: if isinstance(turn, dict) and turn.get("user"): messages.append({"role": "user", "content": str(turn["user"])}) if turn.get("assistant"): messages.append({"role": "assistant", "content": str(turn["assistant"])}) messages.append({"role": "user", "content": result.get("user") or ""}) for idx, step in enumerate(steps): call_id = f"call_{result.get('id', 'web')}_{idx}" messages.append({ "role": "assistant", "content": "", "tool_calls": [{ "id": call_id, "type": "function", "function": { "name": step["tool"], "arguments": json.dumps(step["args"], separators=(",", ":"), ensure_ascii=True), }, }], }) messages.append({"role": "tool", "tool_call_id": call_id, "content": step["output"]}) messages.append({"role": "assistant", "content": final}) row = { "messages": messages, "tools": TOOL_SCHEMAS, "generator": "odysseus_realistic_verbose_web_synthesis_teacher", "metadata": { "source_result_id": result.get("id"), "source_path": result.get("_source_path"), "source_pass": result.get("pass"), "actual_final": result.get("final_answer") or "", }, } row["uuid"] = stable_id("ody_v60_realistic_web_synthesis", row) return row def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--actual", type=Path, action="append", default=[]) parser.add_argument("--out-dir", type=Path, default=DEFAULT_OUT_DIR) parser.add_argument("--db", type=Path, default=REPO_ROOT / "data/app.db") parser.add_argument("--teacher-model", default="") parser.add_argument("--teacher-edits", type=Path) parser.add_argument("--max-cases", type=int, default=180) parser.add_argument("--max-tools", type=int, default=3) args = parser.parse_args() paths = args.actual or DEFAULT_ACTUALS final_by_id = load_teacher_finals(args.teacher_edits) endpoint = None if final_by_id else load_teacher_endpoint(args.db, args.teacher_model or None) results = load_results(paths) candidates = [] for result in results: if result.get("kind") != "web": continue steps = usable_web_steps(result, args.max_tools) if steps and steps[0]["tool"] == "web_search": candidates.append((result, steps)) candidates = candidates[: args.max_cases] rows: list[dict[str, Any]] = [] audits: list[dict[str, Any]] = [] for result, steps in candidates: try: if result.get("id") in final_by_id: edited = { "should_train": True, "final": final_by_id[str(result.get("id"))], "reason": "reused existing teacher-edited final", } else: assert endpoint is not None edited = teacher_final(endpoint, result, steps) row = None if edited.get("should_train") is True: row = build_row(result, steps, str(edited.get("final") or "")) accepted = row is not None if accepted: rows.append(row) audits.append({ "id": result.get("id"), "source_path": result.get("_source_path"), "accepted": accepted, "tool_count": len(steps), "actual_final": result.get("final_answer") or "", "teacher": edited, }) except Exception as exc: audits.append({"id": result.get("id"), "source_path": result.get("_source_path"), "accepted": False, "error": repr(exc)}) print(json.dumps({"processed": len(audits), "accepted": len(rows), "id": result.get("id")}), flush=True) args.out_dir.mkdir(parents=True, exist_ok=True) train: list[dict[str, Any]] = [] val: list[dict[str, Any]] = [] for idx, row in enumerate(rows): (val if idx % 10 == 9 else train).append(row) for name, subset in [("all.jsonl", rows), ("train.jsonl", train), ("val.jsonl", val)]: (args.out_dir / name).write_text("".join(json.dumps(row, ensure_ascii=True) + "\n" for row in subset), encoding="utf-8") (args.out_dir / "audit.json").write_text(json.dumps({"audit": audits}, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") manifest = { "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "source_actuals": [str(path) for path in paths], "candidate_cases": len(candidates), "accepted_sft_rows": len(rows), "train_rows": len(train), "val_rows": len(val), "max_tools": args.max_tools, "goal": "train direct synthesis after realistic verbose web_search/web_fetch outputs", "files": { "train": str(args.out_dir / "train.jsonl"), "val": str(args.out_dir / "val.jsonl"), "all": str(args.out_dir / "all.jsonl"), "audit": str(args.out_dir / "audit.json"), }, } (args.out_dir / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=True, indent=2) + "\n", encoding="utf-8") print(json.dumps(manifest, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())