Files
kamidso-back/kamidso/native_connectors/validator.py
T
javadandCursor edbaa2acec Add native Didar connectors so chats can query CRM data reliably.
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>
2026-10-12 19:26:47 +03:30

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."
),
}