diff --git a/Makefile b/Makefile index a788364..1089c2c 100644 --- a/Makefile +++ b/Makefile @@ -36,6 +36,8 @@ proto: ## Generate Python, TS, and ML stubs from proto definitions --ts_proto_opt=esModuleInterop=true,forceLong=string,outputServices=false,outputJsonMethods=false,outputClientImpl=false,outputEncodeMethods=false,outputPartialMethods=false,outputTypeRegistry=false,onlyTypes=true,snakeToCamel=false \ --proto_path=$(PROTO_DIR) \ $(PROTO_DIR)/agent.proto + # Generate JSON schema from compiled python stubs + $(MAKE) api-schema # ── Auto-Format ────────────────────────────────────────────────── .PHONY: format format-api format-web format-engine format-server format-integrations @@ -99,7 +101,7 @@ inf-health: ## Check ML service health @echo "── STT ──" # ── Studio API Utilities ───────────────────────────────────────── -.PHONY: api-dev api-serve api-migrate api-migration api-clean api-env-schema +.PHONY: api-dev api-serve api-migrate api-migration api-clean api-env-schema api-schema api-dev: ## Install Studio API dev dependencies cd $(API_DIR) && uv sync @@ -111,6 +113,9 @@ api-env-schema: ## Gen Studio API .env.example cd $(API_DIR) && uv run python -m scripts.dump_env_schema > .env.example @echo "✓ .env.example updated" +api-schema: ## Regenerate JSON Schema for Agent configs (from Pydantic + Proto) + cd $(API_DIR) && uv run python -m scripts.generate_schema + api-migrate: ## Run Studio API db migrations cd $(API_DIR) && uv run alembic upgrade head diff --git a/studio/api/app/schemas/agent.py b/studio/api/app/schemas/agent.py index 3c56b87..ca0ee55 100644 --- a/studio/api/app/schemas/agent.py +++ b/studio/api/app/schemas/agent.py @@ -18,7 +18,7 @@ from app.models.agent import AgentStatus -FULL_CONFIG_SCHEMA_URL = "https://feros.ai/schemas/agent-config-v1.schema.json" +FULL_CONFIG_SCHEMA_URL = "https://feros.ai/schemas/agent-config-v3.schema.json" # ── Tool Config (used inside AgentConfig) ────────────────────────── diff --git a/studio/api/scripts/generate_schema.py b/studio/api/scripts/generate_schema.py new file mode 100644 index 0000000..36df312 --- /dev/null +++ b/studio/api/scripts/generate_schema.py @@ -0,0 +1,105 @@ +import json +import os +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) +from google.protobuf.descriptor import FieldDescriptor +from app.schemas.agent import AgentFullConfig +from app.schemas import agent_pb2 + +def field_type_to_schema(field): + if field.type == FieldDescriptor.TYPE_STRING: + return {"type": "string"} + elif field.type == FieldDescriptor.TYPE_BOOL: + return {"type": "boolean"} + elif field.type in (FieldDescriptor.TYPE_INT32, FieldDescriptor.TYPE_UINT32, FieldDescriptor.TYPE_INT64, FieldDescriptor.TYPE_UINT64): + return {"type": "integer"} + elif field.type in (FieldDescriptor.TYPE_DOUBLE, FieldDescriptor.TYPE_FLOAT): + return {"type": "number"} + elif field.type == FieldDescriptor.TYPE_ENUM: + return { + "type": "string", + "enum": [val.name for val in field.enum_type.values], + "description": f"Enum: {field.enum_type.name}" + } + elif field.type == FieldDescriptor.TYPE_MESSAGE: + # Check for map (map fields point to a synthetic nested message type) + if field.message_type.GetOptions().map_entry: + val_field = field.message_type.fields_by_name['value'] + return { + "type": "object", + "additionalProperties": field_type_to_schema(val_field) + } + return get_msg_schema(field.message_type) + return {"type": "string"} # fallback + +def get_msg_schema(desc): + schema = { + "type": "object", + "properties": {}, + "additionalProperties": False, + "description": f"Protobuf Message: {desc.name}" + } + for f in desc.fields: + is_repeated = False + try: + if getattr(f, "label", None) == FieldDescriptor.LABEL_REPEATED: + is_repeated = True + except AttributeError: + is_repeated = getattr(f, "label", 0) == 3 + + if is_repeated and not (f.message_type and f.message_type.GetOptions().map_entry): + fschema = { + "type": "array", + "items": field_type_to_schema(f) + } + else: + fschema = field_type_to_schema(f) + + # Protobuf optional/required semantics mapping (if desired) + # Proto3 makes everything optional except repeated/maps. + + schema["properties"][f.name] = fschema + + return schema + +def main(): + # Base Pydantic wrapper schema + base_schema = AgentFullConfig.model_json_schema() + + # We want to clear out some of Pydantic's title noise at the top level + base_schema.pop("title", None) + + # Generate AgentGraphDef schema + graph_schema = get_msg_schema(agent_pb2.AgentGraphDef.DESCRIPTOR) + graph_schema["description"] = "Agent runtime config (v3_graph) automatically dumped from agent.proto." + graph_schema["additionalProperties"] = True # To be safe with backward compat + + # Inject + base_schema["properties"]["config"] = graph_schema + + # Additional top-level overrides to match strict output + base_schema["$schema"] = "https://json-schema.org/draft/2020-12/schema" + base_schema["$id"] = "https://feros.ai/schemas/agent-config-v3.schema.json" + base_schema["title"] = "Agent Config v3" + base_schema["$comment"] = "DO NOT EDIT. This file is automatically generated by running `make proto` or `make api-schema`." + + # Ensure $schema is required so the import validator can gate on schema version + base_schema.setdefault("required", []) + if "$schema" not in base_schema["required"]: + base_schema["required"].insert(0, "$schema") + + # Ensure properties that were default open in pydantic have false + base_schema["additionalProperties"] = False + + out_path = os.path.join(os.path.dirname(__file__), "..", "..", "web", "public", "schemas", "agent-config-v3.schema.json") + out_path = os.path.abspath(out_path) + + os.makedirs(os.path.dirname(out_path), exist_ok=True) + with open(out_path, "w") as f: + json.dump(base_schema, f, indent=2) + f.write("\n") # ensure trailing newline + + print(f"Generated schema at {out_path}") + +if __name__ == "__main__": + main() diff --git a/studio/web/public/schemas/agent-config-v1.schema.json b/studio/web/public/schemas/agent-config-v1.schema.json deleted file mode 100644 index a6b50ce..0000000 --- a/studio/web/public/schemas/agent-config-v1.schema.json +++ /dev/null @@ -1,146 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://feros.ai/schemas/agent-config-v1.schema.json", - "title": "Agent Config v1", - "description": "Portable full export/import payload for Feros agents.", - "type": "object", - "additionalProperties": false, - "required": [ - "$schema", - "name", - "config" - ], - "properties": { - "$schema": { - "type": "string", - "const": "https://feros.ai/schemas/agent-config-v1.schema.json" - }, - "name": { - "type": "string", - "minLength": 1 - }, - "description": { - "type": ["string", "null"] - }, - "config": { - "type": "object", - "description": "Agent runtime config (v3_graph).", - "additionalProperties": true, - "properties": { - "entry": { - "type": "string", - "minLength": 1 - }, - "nodes": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": true, - "properties": { - "system_prompt": { - "type": "string" - }, - "greeting": { - "type": "string" - }, - "tools": { - "type": "array", - "items": { - "type": "string" - } - }, - "edges": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - }, - "tools": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": true, - "properties": { - "description": { - "type": "string" - }, - "script": { - "type": "string" - }, - "params": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": true, - "properties": { - "name": { - "type": "string" - }, - "type": { - "type": "string" - }, - "description": { - "type": "string" - }, - "required": { - "type": "boolean" - }, - "options": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - }, - "cancel_on_barge_in": { - "type": "boolean" - }, - "side_effect": { - "type": "boolean" - }, - "summarize_result": { - "type": "boolean", - "description": "When true, long tool output is summarized by AI instead of being truncated." - } - } - } - } - } - }, - "mermaid_diagram": { - "type": ["string", "null"] - }, - "connections": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "required": ["provider", "is_default", "status"], - "properties": { - "provider": { - "type": "string", - "minLength": 1 - }, - "name": { - "type": ["string", "null"] - }, - "auth_type": { - "type": ["string", "null"] - }, - "is_default": { - "type": "boolean" - }, - "status": { - "type": "string", - "enum": ["connected", "inherited", "missing"] - } - } - } - } - } -} diff --git a/studio/web/public/schemas/agent-config-v3.schema.json b/studio/web/public/schemas/agent-config-v3.schema.json new file mode 100644 index 0000000..491f121 --- /dev/null +++ b/studio/web/public/schemas/agent-config-v3.schema.json @@ -0,0 +1,282 @@ +{ + "$defs": { + "ImportedConnection": { + "description": "Connection metadata included in full config export/import.", + "properties": { + "provider": { + "title": "Provider", + "type": "string" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Name" + }, + "auth_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Auth Type" + }, + "is_default": { + "default": false, + "title": "Is Default", + "type": "boolean" + }, + "status": { + "default": "missing", + "enum": [ + "connected", + "inherited", + "missing" + ], + "title": "Status", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "ImportedConnection", + "type": "object" + } + }, + "description": "Superset export/import payload for agent portability.", + "properties": { + "$schema": { + "default": "https://feros.ai/schemas/agent-config-v3.schema.json", + "description": "Canonical JSON Schema URL for this payload format.", + "title": "$Schema", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Description" + }, + "config": { + "type": "object", + "properties": { + "entry": { + "type": "string" + }, + "nodes": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "system_prompt": { + "type": "string" + }, + "tools": { + "type": "array", + "items": { + "type": "string" + } + }, + "edges": { + "type": "array", + "items": { + "type": "string" + } + }, + "model": { + "type": "string" + }, + "temperature": { + "type": "number" + }, + "max_tokens": { + "type": "integer" + }, + "voice_id": { + "type": "string" + }, + "greeting": { + "type": "string" + } + }, + "additionalProperties": false, + "description": "Protobuf Message: NodeDef" + } + }, + "tools": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "script": { + "type": "string" + }, + "params": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "options": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "description": "Protobuf Message: ParamDef" + } + }, + "cancel_on_barge_in": { + "type": "boolean" + }, + "side_effect": { + "type": "boolean" + }, + "summarize_result": { + "type": "boolean" + } + }, + "additionalProperties": false, + "description": "Protobuf Message: ToolDef" + } + }, + "language": { + "type": "string" + }, + "timezone": { + "type": "string" + }, + "voice_id": { + "type": "string" + }, + "tts_provider": { + "type": "string" + }, + "tts_model": { + "type": "string" + }, + "recording": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "output_uri": { + "type": "string" + }, + "audio_layout": { + "type": "string", + "enum": [ + "AUDIO_LAYOUT_UNSPECIFIED", + "AUDIO_LAYOUT_STEREO", + "AUDIO_LAYOUT_MONO" + ], + "description": "Enum: AudioLayout" + }, + "sample_rate": { + "type": "integer" + }, + "audio_format": { + "type": "string", + "enum": [ + "AUDIO_FORMAT_UNSPECIFIED", + "AUDIO_FORMAT_OPUS", + "AUDIO_FORMAT_WAV" + ], + "description": "Enum: AudioFormat" + }, + "max_duration_secs": { + "type": "integer" + }, + "save_transcript": { + "type": "boolean" + }, + "include_tool_details": { + "type": "boolean" + }, + "include_llm_metadata": { + "type": "boolean" + } + }, + "additionalProperties": false, + "description": "Protobuf Message: RecordingConfig" + }, + "config_schema_version": { + "type": "string" + }, + "gemini_live_model": { + "type": "string" + } + }, + "additionalProperties": true, + "description": "Agent runtime config (v3_graph) automatically dumped from agent.proto." + }, + "mermaid_diagram": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mermaid Diagram" + }, + "connections": { + "items": { + "$ref": "#/$defs/ImportedConnection" + }, + "title": "Connections", + "type": "array" + } + }, + "required": [ + "$schema", + "name", + "config" + ], + "type": "object", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://feros.ai/schemas/agent-config-v3.schema.json", + "title": "Agent Config v3", + "$comment": "DO NOT EDIT. This file is automatically generated by running `make proto` or `make api-schema`.", + "additionalProperties": false +} diff --git a/studio/web/src/app/dashboard/agents/import/page.tsx b/studio/web/src/app/dashboard/agents/import/page.tsx index 9d44288..2c754a1 100644 --- a/studio/web/src/app/dashboard/agents/import/page.tsx +++ b/studio/web/src/app/dashboard/agents/import/page.tsx @@ -82,7 +82,7 @@ export default function ImportAgentPage() { setFullConfig(null); setParsedConfig(null); setParseError( - "Import expects a full config export payload with $schema set to agent-config-v1." + "Import expects a full config export payload with $schema set to agent-config-v3." ); } setParseStatus(nextStatus); diff --git a/studio/web/src/lib/api/client.ts b/studio/web/src/lib/api/client.ts index 698b021..f0e32a7 100644 --- a/studio/web/src/lib/api/client.ts +++ b/studio/web/src/lib/api/client.ts @@ -100,8 +100,7 @@ export interface ImportedConnection { status: "connected" | "inherited" | "missing"; } -export const FULL_CONFIG_SCHEMA_URL = - "https://feros.ai/schemas/agent-config-v1.schema.json" as const; +export const FULL_CONFIG_SCHEMA_URL = "https://feros.ai/schemas/agent-config-v3.schema.json" as const; export interface AgentFullConfig { $schema: typeof FULL_CONFIG_SCHEMA_URL;