mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-16 12:50:48 +08:00
refactor: root-package imports (svc/runtime/interfaces) and fix PYTHONPATH
- Rename platform/ to svc/ to avoid shadowing stdlib platform. - Replace from oclaw.* with from svc/runtime/interfaces; update -m CLI paths. - tests/conftest: prepend repo root to sys.path (no parent-folder package name). - CI: paths and offline_eval script under repo root. - Ops scripts: PYTHONPATH must be repo root for python -m runtime.* (fixes gateway/WhatsApp sidecar startup). - Fix default oclaw.json path in tabular/file attachment limits; stabilize attachment test config. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
7941b27f58
commit
420abac9f1
405 changed files with 1316 additions and 1345 deletions
205
svc/llm/tool_schema.py
Normal file
205
svc/llm/tool_schema.py
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Full minimal object schema for ``function.parameters`` (wire / shrink / tier_minimal).
|
||||
# A bare ``{type, additionalProperties}`` alone has led strict gateways to emit or validate
|
||||
# ``required: null`` / missing array fields — always send explicit ``properties`` + ``required``.
|
||||
_MIN_TOOL_PARAMETERS: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
||||
_COMBO_KEYS = frozenset({"allOf", "anyOf", "oneOf", "prefixItems"})
|
||||
|
||||
|
||||
def _schema_type_tags(t: Any) -> set[str]:
|
||||
if t is None:
|
||||
return set()
|
||||
if isinstance(t, list):
|
||||
return {str(x).strip().lower() for x in t if x is not None and str(x).strip()}
|
||||
s = str(t).strip().lower()
|
||||
return {s} if s else set()
|
||||
|
||||
|
||||
def complete_json_schema_for_openai_tools(obj: Any) -> Any:
|
||||
"""Recursively ensure JSON-schema fragments match what strict OpenAI-compat gateways expect.
|
||||
|
||||
This is **structural completion** for our wire path (tier/shrink/SDK), not a general MCP fix-all:
|
||||
every ``type: object`` node gets explicit ``properties`` + ``required`` (list); every
|
||||
``type: array`` gets ``items`` (object). Combo keywords that must be arrays and are ``null``
|
||||
are dropped; list elements that are ``null`` are removed.
|
||||
"""
|
||||
|
||||
if isinstance(obj, dict):
|
||||
out: dict[str, Any] = {}
|
||||
for k, v in obj.items():
|
||||
key = str(k)
|
||||
if key in _COMBO_KEYS and v is None:
|
||||
if key in ("allOf", "prefixItems"):
|
||||
out[key] = []
|
||||
continue
|
||||
out[key] = complete_json_schema_for_openai_tools(v)
|
||||
|
||||
for combo in _COMBO_KEYS:
|
||||
if combo not in out:
|
||||
continue
|
||||
val = out[combo]
|
||||
if not isinstance(val, list):
|
||||
continue
|
||||
cleaned = [complete_json_schema_for_openai_tools(x) for x in val if x is not None]
|
||||
if combo in ("anyOf", "oneOf") and not cleaned:
|
||||
out.pop(combo, None)
|
||||
else:
|
||||
out[combo] = cleaned
|
||||
|
||||
if isinstance(out.get("properties"), dict):
|
||||
t0 = out.get("type")
|
||||
if t0 is None or (isinstance(t0, str) and not str(t0).strip()):
|
||||
out["type"] = "object"
|
||||
|
||||
tags = _schema_type_tags(out.get("type"))
|
||||
if "object" in tags:
|
||||
if not isinstance(out.get("properties"), dict):
|
||||
out["properties"] = {}
|
||||
r = out.get("required")
|
||||
if r is None or not isinstance(r, list):
|
||||
out["required"] = []
|
||||
if "array" in tags:
|
||||
if out.get("items") is None:
|
||||
out["items"] = {}
|
||||
elif isinstance(out.get("items"), dict):
|
||||
out["items"] = complete_json_schema_for_openai_tools(out["items"])
|
||||
return out
|
||||
if isinstance(obj, list):
|
||||
return [complete_json_schema_for_openai_tools(x) for x in obj]
|
||||
return obj
|
||||
|
||||
|
||||
def complete_openai_tools_wire_parameters(tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""Apply :func:`complete_json_schema_for_openai_tools` to each function's ``parameters``."""
|
||||
out: list[dict[str, Any]] = []
|
||||
for ent in tools or []:
|
||||
if not isinstance(ent, dict):
|
||||
continue
|
||||
if str(ent.get("type") or "") != "function":
|
||||
out.append(ent)
|
||||
continue
|
||||
fn = ent.get("function")
|
||||
if not isinstance(fn, dict):
|
||||
out.append(ent)
|
||||
continue
|
||||
params = fn.get("parameters")
|
||||
if not isinstance(params, dict):
|
||||
row = dict(ent)
|
||||
row["function"] = {**dict(fn), "parameters": dict(_MIN_TOOL_PARAMETERS)}
|
||||
out.append(row)
|
||||
continue
|
||||
row = dict(ent)
|
||||
row["function"] = {**dict(fn), "parameters": complete_json_schema_for_openai_tools(dict(params))}
|
||||
out.append(row)
|
||||
return out
|
||||
|
||||
|
||||
def openai_tools_json_byte_length(tools: list[dict[str, Any]]) -> int:
|
||||
try:
|
||||
return len(json.dumps(tools, ensure_ascii=False, default=str))
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def shrink_openai_tools_payload_for_api(tools: list[dict[str, Any]], *, max_json_bytes: int) -> list[dict[str, Any]]:
|
||||
"""Shrink OpenAI ``tools`` JSON for strict OpenAI-compatible gateways."""
|
||||
if not tools or max_json_bytes <= 0:
|
||||
return tools
|
||||
if openai_tools_json_byte_length(tools) <= max_json_bytes:
|
||||
return tools
|
||||
flat: list[dict[str, Any]] = []
|
||||
for t in tools:
|
||||
if not isinstance(t, dict) or str(t.get("type") or "") != "function":
|
||||
continue
|
||||
fn = t.get("function")
|
||||
if not isinstance(fn, dict):
|
||||
continue
|
||||
name = str(fn.get("name") or "").strip()
|
||||
if not name:
|
||||
continue
|
||||
desc = str(fn.get("description") or "")
|
||||
flat.append({"type": "function", "function": {"name": name, "description": desc, "parameters": dict(_MIN_TOOL_PARAMETERS)}})
|
||||
if not flat:
|
||||
return tools
|
||||
|
||||
def _serialized_with_desc_cap(cap: int) -> str:
|
||||
trimmed: list[dict[str, Any]] = []
|
||||
for x in flat:
|
||||
fn0 = x["function"]
|
||||
trimmed.append(
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": fn0["name"],
|
||||
"description": (fn0.get("description") or "")[: max(0, cap)],
|
||||
"parameters": fn0["parameters"],
|
||||
},
|
||||
}
|
||||
)
|
||||
return json.dumps(trimmed, ensure_ascii=False, default=str)
|
||||
|
||||
lo, hi = 0, min(8000, max_json_bytes)
|
||||
best_cap = 0
|
||||
while lo <= hi:
|
||||
mid = (lo + hi) // 2
|
||||
s = _serialized_with_desc_cap(mid)
|
||||
if len(s) <= max_json_bytes:
|
||||
best_cap = mid
|
||||
lo = mid + 1
|
||||
else:
|
||||
hi = mid - 1
|
||||
out_obj: list[dict[str, Any]] = json.loads(_serialized_with_desc_cap(best_cap))
|
||||
blob = json.dumps(out_obj, ensure_ascii=False, default=str)
|
||||
dropped = 0
|
||||
while len(out_obj) > 8 and len(blob) > max_json_bytes:
|
||||
out_obj.pop()
|
||||
dropped += 1
|
||||
blob = json.dumps(out_obj, ensure_ascii=False, default=str)
|
||||
logger.warning(
|
||||
"Shrunk OpenAI tools[] for API size limits: count=%d max_json_bytes=%d desc_cap=%d dropped=%d final_bytes=%d",
|
||||
len(out_obj),
|
||||
max_json_bytes,
|
||||
best_cap,
|
||||
dropped,
|
||||
len(blob),
|
||||
)
|
||||
return out_obj
|
||||
|
||||
|
||||
def default_max_openai_tools_json_bytes(base_url: str | None) -> int | None:
|
||||
raw = str(os.getenv("AIA_OPENAI_TOOLS_MAX_JSON_CHARS") or "").strip()
|
||||
if raw.isdigit():
|
||||
return max(2000, int(raw))
|
||||
u = (base_url or "").lower()
|
||||
if "dashscope.aliyuncs.com" in u:
|
||||
return 28000
|
||||
if str(os.getenv("AIA_SHRINK_OPENAI_TOOLS") or "").strip().lower() in ("1", "true", "yes", "on"):
|
||||
mx = str(os.getenv("AIA_SHRINK_OPENAI_TOOLS_MAX_JSON") or "28000").strip()
|
||||
return max(2000, int(mx)) if mx.isdigit() else 28000
|
||||
return None
|
||||
|
||||
|
||||
__all__ = [
|
||||
"MIN_OPENAI_FUNCTION_PARAMETERS",
|
||||
"complete_json_schema_for_openai_tools",
|
||||
"complete_openai_tools_wire_parameters",
|
||||
"default_max_openai_tools_json_bytes",
|
||||
"openai_tools_json_byte_length",
|
||||
"shrink_openai_tools_payload_for_api",
|
||||
]
|
||||
|
||||
MIN_OPENAI_FUNCTION_PARAMETERS = _MIN_TOOL_PARAMETERS
|
||||
Loading…
Add table
Add a link
Reference in a new issue