Connector calls, semantic rules, and DuckDB memory keep long runs stable, and stream retry stops chats from stalling. Co-authored-by: Cursor <cursoragent@cursor.com>
128 lines
4.4 KiB
Python
128 lines
4.4 KiB
Python
"""JSON Schema checks and actionable validation payloads."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
from jsonschema import Draft202012Validator
|
|
from jsonschema.exceptions import SchemaError, ValidationError
|
|
|
|
from kamidso.native_connectors.errors import CatalogError
|
|
from kamidso.native_connectors.models import ConnectorFunction
|
|
|
|
_COMPILED: Dict[str, Draft202012Validator] = {}
|
|
|
|
|
|
def check_input_schema(function_name: str, schema: Dict[str, Any]) -> None:
|
|
if not isinstance(schema, dict) or schema.get("type") != "object":
|
|
raise CatalogError(
|
|
f"Function '{function_name}' input_schema must be a JSON object schema."
|
|
)
|
|
try:
|
|
Draft202012Validator.check_schema(schema)
|
|
except SchemaError as exc:
|
|
raise CatalogError(
|
|
f"Function '{function_name}' has an invalid input schema: {exc.message}"
|
|
) from exc
|
|
|
|
|
|
def compiled_validator(function: ConnectorFunction) -> Draft202012Validator:
|
|
cached = _COMPILED.get(function.name)
|
|
schema = function.input_schema
|
|
if cached is not None and cached.schema == schema:
|
|
return cached
|
|
validator = Draft202012Validator(schema)
|
|
_COMPILED[function.name] = validator
|
|
return validator
|
|
|
|
|
|
def clear_validator_cache() -> None:
|
|
_COMPILED.clear()
|
|
|
|
|
|
def example_skeleton(schema: Dict[str, Any]) -> Any:
|
|
if not isinstance(schema, dict):
|
|
return {}
|
|
if "default" in schema and schema.get("type") != "object":
|
|
return schema["default"]
|
|
kind = schema.get("type")
|
|
if kind == "object":
|
|
properties = schema.get("properties") or {}
|
|
required = schema.get("required") or []
|
|
skeleton: Dict[str, Any] = {}
|
|
for key in required:
|
|
skeleton[key] = example_skeleton(properties.get(key) or {"type": "object"})
|
|
return skeleton
|
|
if kind == "array":
|
|
return []
|
|
if kind == "integer":
|
|
minimum = schema.get("minimum", 0)
|
|
maximum = schema.get("maximum")
|
|
if isinstance(maximum, int) and maximum <= 100 and isinstance(minimum, int):
|
|
return maximum
|
|
return minimum if isinstance(minimum, int) else 0
|
|
if kind == "number":
|
|
minimum = schema.get("minimum", 0)
|
|
return minimum
|
|
if kind == "boolean":
|
|
return False
|
|
if kind == "string":
|
|
enum = schema.get("enum")
|
|
if isinstance(enum, list) and enum:
|
|
return enum[0]
|
|
return ""
|
|
return {}
|
|
|
|
|
|
def _issue(error: ValidationError) -> Dict[str, Any]:
|
|
path = list(error.path)
|
|
message = error.message
|
|
if error.validator == "additionalProperties" and isinstance(error.instance, dict):
|
|
schema = error.schema if isinstance(error.schema, dict) else {}
|
|
allowed = set((schema.get("properties") or {}).keys())
|
|
extras = sorted(key for key in error.instance.keys() if key not in allowed)
|
|
if extras:
|
|
message = "Unexpected fields: " + ", ".join(extras)
|
|
return {"path": path, "message": message}
|
|
|
|
|
|
def validate_body(
|
|
function: ConnectorFunction,
|
|
body: Any,
|
|
) -> Tuple[Optional[Dict[str, Any]], List[Dict[str, Any]]]:
|
|
"""Return ``(normalized_body, issues)``. Issues empty means the body is valid."""
|
|
if body is None:
|
|
body = {}
|
|
if isinstance(body, str):
|
|
return None, [{"path": [], "message": "Body must be a JSON object, not a string."}]
|
|
if not isinstance(body, dict):
|
|
return None, [{"path": [], "message": "Body must be a JSON object."}]
|
|
validator = compiled_validator(function)
|
|
errors = sorted(validator.iter_errors(body), key=lambda item: list(item.path))
|
|
if not errors:
|
|
return body, []
|
|
return None, [_issue(error) for error in errors]
|
|
|
|
|
|
def validation_payload(
|
|
*,
|
|
connector_name: str,
|
|
function: ConnectorFunction,
|
|
issues: List[Dict[str, Any]],
|
|
) -> Dict[str, Any]:
|
|
return {
|
|
"ok": False,
|
|
"error_type": "validation_error",
|
|
"connector": connector_name,
|
|
"function": function.name,
|
|
"message": "Request body does not match the predefined schema.",
|
|
"issues": issues,
|
|
"expected_schema": function.input_schema,
|
|
"example_skeleton": example_skeleton(function.input_schema),
|
|
"semantic_hints": list(function.semantic_hints),
|
|
"http_sent": False,
|
|
"instruction": (
|
|
"Fix the body and retry the SAME function. Do not invent another function."
|
|
),
|
|
}
|