Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion studio/api/app/schemas/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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) ──────────────────────────

Expand Down
105 changes: 105 additions & 0 deletions studio/api/scripts/generate_schema.py
Original file line number Diff line number Diff line change
@@ -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()
146 changes: 0 additions & 146 deletions studio/web/public/schemas/agent-config-v1.schema.json

This file was deleted.

Loading
Loading