diff --git a/src/praisonai/praisonai/auto.py b/src/praisonai/praisonai/auto.py index f061685d0..3df99c99f 100644 --- a/src/praisonai/praisonai/auto.py +++ b/src/praisonai/praisonai/auto.py @@ -1107,11 +1107,26 @@ def convert_and_save(self, json_data, merge=False): for task_id, task_details in role_details['tasks'].items(): yaml_data['roles'][role_id]['tasks'][task_id] = self._format_task(task_details) - # Save to YAML file atomically, maintaining the order - _atomic_write_text( - self.agent_file, - lambda f: _yaml_dump(yaml_data, f, allow_unicode=True, sort_keys=False), - ) + # Save to YAML file atomically, maintaining the order. Prepend the + # yaml-language-server header so editors give autocomplete / inline + # validation for the scaffolded agents.yaml out of the box. A leading + # YAML comment is ignored by yaml.safe_load, so execution is unaffected. + def _write_with_header(f): + try: + from .config.schema import AGENTS_SCHEMA_HEADER + f.write(AGENTS_SCHEMA_HEADER) + except Exception as exc: + # Non-fatal: the YAML is still valid/executable without the + # editor header, but surface it so a missing schema directive + # is diagnosable instead of silently dropped. + logger.debug( + "Could not write yaml-language-server schema header to %s: %s", + self.agent_file, + exc, + ) + _yaml_dump(yaml_data, f, allow_unicode=True, sort_keys=False) + + _atomic_write_text(self.agent_file, _write_with_header) def merge_with_existing_agents(self, new_json_data): """ diff --git a/src/praisonai/praisonai/cli/commands/validate.py b/src/praisonai/praisonai/cli/commands/validate.py index 9e3c427c7..cf145129b 100644 --- a/src/praisonai/praisonai/cli/commands/validate.py +++ b/src/praisonai/praisonai/cli/commands/validate.py @@ -227,7 +227,20 @@ def check( @app.command() -def schema(): +def schema( + agents: bool = typer.Option( + False, + "--agents", + help="Emit the machine-readable JSON Schema for agents.yaml " + "(derived from YAMLConfig) instead of the human-readable summary.", + ), + output: Optional[str] = typer.Option( + None, + "--output", "-o", + help="Write the JSON Schema to a file instead of stdout " + "(implies --agents; useful for offline/pinned editor setups).", + ), +): """ Display the YAML configuration schema documentation. @@ -236,10 +249,34 @@ def schema(): - Task configuration - Workflow configuration - Global settings + + With --agents (or --output), emit the machine-readable JSON Schema for + agents.yaml so you can point your editor's YAML language server at a local + file: + + praisonai validate schema --agents # print to stdout + praisonai validate schema -o agents.schema.json # write to a file """ from praisonai.config.schema import YAMLConfig, AgentConfig, TaskConfig import json - + + # Emit the machine-readable JSON Schema for offline/pinned editor setups. + if agents or output: + from praisonai.config.schema import generate_agents_schema + + schema_json = json.dumps(generate_agents_schema(), indent=2) + if output: + out_path = Path(output) + try: + out_path.write_text(schema_json + "\n", encoding="utf-8") + except OSError as exc: + console.print(f"[red]✗ Failed to write {out_path}:[/red] {exc}") + sys.exit(1) + console.print(f"[green]✓ Wrote agents JSON Schema to[/green] {out_path}") + else: + sys.stdout.write(schema_json + "\n") + return + console.print("[bold cyan]PraisonAI YAML Configuration Schema[/bold cyan]\n") # Display main sections diff --git a/src/praisonai/praisonai/config/__init__.py b/src/praisonai/praisonai/config/__init__.py index f75488f56..e030edea4 100644 --- a/src/praisonai/praisonai/config/__init__.py +++ b/src/praisonai/praisonai/config/__init__.py @@ -17,6 +17,9 @@ RuntimeConfig, CliBackendConfig, GlobalConfig, + AGENTS_SCHEMA_URL, + AGENTS_SCHEMA_HEADER, + generate_agents_schema, ) from .validator import ConfigValidator @@ -37,4 +40,7 @@ 'CliBackendConfig', 'GlobalConfig', 'ConfigValidator', + 'AGENTS_SCHEMA_URL', + 'AGENTS_SCHEMA_HEADER', + 'generate_agents_schema', ] \ No newline at end of file diff --git a/src/praisonai/praisonai/config/agents.schema.json b/src/praisonai/praisonai/config/agents.schema.json new file mode 100644 index 000000000..8fc2bfc4b --- /dev/null +++ b/src/praisonai/praisonai/config/agents.schema.json @@ -0,0 +1,1330 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/MervinPraison/PraisonAI/main/src/praisonai/praisonai/config/agents.schema.json", + "$defs": { + "AgentConfig": { + "description": "Configuration for a single agent/role.", + "properties": { + "role": { + "description": "Agent role", + "title": "Role", + "type": "string" + }, + "goal": { + "description": "Agent goal", + "title": "Goal", + "type": "string" + }, + "backstory": { + "description": "Agent backstory", + "title": "Backstory", + "type": "string" + }, + "instructions": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Additional instructions (alias for backstory)", + "title": "Instructions" + }, + "tools": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "List of tools the agent can use", + "title": "Tools" + }, + "toolsets": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "List of toolsets the agent can use", + "title": "Toolsets" + }, + "llm": { + "anyOf": [ + { + "type": "string" + }, + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "LLM model to use (string or dict with 'model' key)", + "title": "Llm" + }, + "function_calling_llm": { + "anyOf": [ + { + "type": "string" + }, + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "LLM for function calling (string or dict with 'model' key)", + "title": "Function Calling Llm" + }, + "tasks": { + "anyOf": [ + { + "additionalProperties": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "$ref": "#/$defs/TaskConfig" + } + ] + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Tasks assigned to this agent", + "title": "Tasks" + }, + "allow_delegation": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": true, + "description": "Allow delegation to other agents", + "title": "Allow Delegation" + }, + "max_iter": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 10, + "description": "Maximum iterations", + "title": "Max Iter" + }, + "max_rpm": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 60, + "description": "Maximum requests per minute", + "title": "Max Rpm" + }, + "max_execution_time": { + "anyOf": [ + { + "minimum": 0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Maximum execution time", + "title": "Max Execution Time" + }, + "verbose": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "description": "Verbose output", + "title": "Verbose" + }, + "cache": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": true, + "description": "Enable caching", + "title": "Cache" + }, + "streaming": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "description": "Enable streaming", + "title": "Streaming" + }, + "stream": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Alias for streaming", + "title": "Stream" + }, + "tool_timeout": { + "anyOf": [ + { + "minimum": 0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Tool execution timeout", + "title": "Tool Timeout" + }, + "tool_retry_policy": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "$ref": "#/$defs/ToolRetryPolicy" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Tool retry policy", + "title": "Tool Retry Policy" + }, + "planning_tools": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Planning tools", + "title": "Planning Tools" + }, + "planning": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "description": "Enable planning mode", + "title": "Planning" + }, + "autonomy": { + "anyOf": [ + { + "maximum": 10, + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 0, + "description": "Autonomy level (0-10)", + "title": "Autonomy" + }, + "guardrails": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Guardrails to apply", + "title": "Guardrails" + }, + "approval": { + "anyOf": [ + { + "type": "boolean" + }, + { + "additionalProperties": true, + "type": "object" + }, + { + "$ref": "#/$defs/ApprovalConfig" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Approval configuration", + "title": "Approval" + }, + "skills": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Skills the agent has", + "title": "Skills" + }, + "reflection": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "description": "Enable reflection", + "title": "Reflection" + }, + "handoff": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "$ref": "#/$defs/HandoffConfig" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Handoff configuration", + "title": "Handoff" + }, + "web": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "description": "Enable web access", + "title": "Web" + }, + "web_fetch": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "description": "Enable web fetching", + "title": "Web Fetch" + }, + "cli_backend": { + "anyOf": [ + { + "type": "string" + }, + { + "additionalProperties": true, + "type": "object" + }, + { + "$ref": "#/$defs/CliBackendConfig" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CLI backend config", + "title": "Cli Backend" + }, + "runtime": { + "anyOf": [ + { + "type": "string" + }, + { + "additionalProperties": true, + "type": "object" + }, + { + "$ref": "#/$defs/RuntimeConfig" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Runtime configuration", + "title": "Runtime" + }, + "system_template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "System prompt template", + "title": "System Template" + }, + "prompt_template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Prompt template", + "title": "Prompt Template" + }, + "response_template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Response template", + "title": "Response Template" + } + }, + "title": "AgentConfig", + "type": "object" + }, + "ApprovalConfig": { + "description": "Configuration for agent approval requirements.", + "properties": { + "enabled": { + "default": false, + "description": "Enable approval mode", + "title": "Enabled", + "type": "boolean" + }, + "timeout": { + "anyOf": [ + { + "minimum": 0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": 300.0, + "description": "Approval timeout in seconds", + "title": "Timeout" + }, + "level": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "tool", + "description": "Approval level (tool/step/all)", + "title": "Level" + }, + "auto_approve": { + "description": "Auto-approved tools", + "items": { + "type": "string" + }, + "title": "Auto Approve", + "type": "array" + } + }, + "title": "ApprovalConfig", + "type": "object" + }, + "CliBackendConfig": { + "description": "Configuration for CLI backend.", + "properties": { + "type": { + "description": "CLI backend type", + "title": "Type", + "type": "string" + }, + "config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Backend-specific config", + "title": "Config" + } + }, + "required": [ + "type" + ], + "title": "CliBackendConfig", + "type": "object" + }, + "GlobalConfig": { + "description": "Global configuration settings.", + "properties": { + "acp": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "description": "Enable ACP mode", + "title": "Acp" + }, + "lsp": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "description": "Enable LSP mode", + "title": "Lsp" + } + }, + "title": "GlobalConfig", + "type": "object" + }, + "HandoffConfig": { + "description": "Configuration for agent handoff behavior.", + "properties": { + "to": { + "description": "List of agent roles to handoff to", + "items": { + "type": "string" + }, + "title": "To", + "type": "array" + }, + "policy": { + "anyOf": [ + { + "$ref": "#/$defs/HandoffPolicy" + }, + { + "type": "null" + } + ], + "default": "any", + "description": "Handoff policy" + }, + "timeout": { + "anyOf": [ + { + "minimum": 0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": 300.0, + "description": "Handoff timeout in seconds", + "title": "Timeout" + }, + "max_depth": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 5, + "description": "Maximum handoff depth", + "title": "Max Depth" + }, + "max_concurrent": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 3, + "description": "Maximum concurrent handoffs", + "title": "Max Concurrent" + }, + "detect_cycles": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": true, + "description": "Detect handoff cycles", + "title": "Detect Cycles" + } + }, + "title": "HandoffConfig", + "type": "object" + }, + "HandoffPolicy": { + "description": "Handoff policy for agent delegation.", + "enum": [ + "any", + "all", + "round_robin", + "least_busy" + ], + "title": "HandoffPolicy", + "type": "string" + }, + "ProcessType": { + "description": "Process type for task execution.", + "enum": [ + "sequential", + "hierarchical", + "consensual", + "workflow" + ], + "title": "ProcessType", + "type": "string" + }, + "RuntimeConfig": { + "description": "Configuration for agent runtime environment.", + "properties": { + "type": { + "description": "Runtime type (docker/sandbox/local)", + "title": "Type", + "type": "string" + }, + "image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Runtime image", + "title": "Image" + }, + "env": { + "additionalProperties": { + "type": "string" + }, + "description": "Environment variables", + "title": "Env", + "type": "object" + } + }, + "required": [ + "type" + ], + "title": "RuntimeConfig", + "type": "object" + }, + "TaskConfig": { + "description": "Configuration for a single task.", + "properties": { + "description": { + "description": "Task description", + "title": "Description", + "type": "string" + }, + "agent": { + "description": "Agent to execute the task", + "title": "Agent", + "type": "string" + }, + "expected_output": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Expected output format", + "title": "Expected Output" + }, + "tools": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Tools to use for this task", + "title": "Tools" + }, + "context": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Context from other tasks", + "title": "Context" + }, + "output_file": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Output file path", + "title": "Output File" + }, + "async_execution": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "description": "Execute asynchronously", + "title": "Async Execution" + }, + "condition": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Condition for task execution", + "title": "Condition" + } + }, + "required": [ + "description", + "agent" + ], + "title": "TaskConfig", + "type": "object" + }, + "ToolRetryPolicy": { + "description": "Configuration for tool retry behavior.", + "properties": { + "max_attempts": { + "default": 3, + "description": "Maximum retry attempts", + "minimum": 1, + "title": "Max Attempts", + "type": "integer" + }, + "delay": { + "default": 1.0, + "description": "Delay between retries in seconds", + "minimum": 0, + "title": "Delay", + "type": "number" + }, + "backoff_factor": { + "default": 2.0, + "description": "Exponential backoff factor", + "minimum": 1, + "title": "Backoff Factor", + "type": "number" + }, + "max_delay": { + "default": 60.0, + "description": "Maximum delay between retries", + "minimum": 0, + "title": "Max Delay", + "type": "number" + } + }, + "title": "ToolRetryPolicy", + "type": "object" + }, + "WorkflowConfig": { + "description": "Configuration for workflow execution.", + "properties": { + "default_llm": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Default LLM for workflow", + "title": "Default Llm" + }, + "timeout": { + "anyOf": [ + { + "minimum": 0, + "type": "number" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Workflow timeout", + "title": "Timeout" + }, + "max_parallel": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 3, + "description": "Maximum parallel executions", + "title": "Max Parallel" + }, + "error_handling": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "stop", + "description": "Error handling strategy", + "title": "Error Handling" + } + }, + "title": "WorkflowConfig", + "type": "object" + }, + "WorkflowStep": { + "description": "Configuration for a workflow step.", + "properties": { + "name": { + "description": "Step name", + "title": "Name", + "type": "string" + }, + "type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "task", + "description": "Step type (task/route/parallel/loop)", + "title": "Type" + }, + "agent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Agent for task steps", + "title": "Agent" + }, + "task": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Task description", + "title": "Task" + }, + "steps": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/WorkflowStep" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Sub-steps for complex types", + "title": "Steps" + }, + "condition": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Condition for step execution", + "title": "Condition" + }, + "routes": { + "anyOf": [ + { + "additionalProperties": { + "items": { + "$ref": "#/$defs/WorkflowStep" + }, + "type": "array" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Routes for routing steps", + "title": "Routes" + }, + "count": { + "anyOf": [ + { + "minimum": 1, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Loop count", + "title": "Count" + } + }, + "required": [ + "name" + ], + "title": "WorkflowStep", + "type": "object" + } + }, + "description": "Schema for agents.yaml consumed by the PraisonAI agent runtime (roles/agents, tasks, tools, llm, workflow).", + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Configuration name", + "title": "Name" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Configuration description", + "title": "Description" + }, + "framework": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "praisonai", + "description": "Framework to use", + "title": "Framework" + }, + "process": { + "anyOf": [ + { + "$ref": "#/$defs/ProcessType" + }, + { + "type": "null" + } + ], + "default": "sequential", + "description": "Process type" + }, + "roles": { + "anyOf": [ + { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/$defs/AgentConfig" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Roles" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/AgentConfig" + } + } + ], + "description": "Agent roles (canonical)" + }, + "agents": { + "anyOf": [ + { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/$defs/AgentConfig" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Agents" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/AgentConfig" + } + } + ], + "description": "Agents (backward compat)" + }, + "tasks": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/TaskConfig" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Task definitions", + "title": "Tasks" + }, + "workflow": { + "anyOf": [ + { + "$ref": "#/$defs/WorkflowConfig" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Workflow configuration" + }, + "steps": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/WorkflowStep" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Workflow steps", + "title": "Steps" + }, + "input": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Input/topic (canonical)", + "title": "Input" + }, + "topic": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Topic (backward compat)", + "title": "Topic" + }, + "tools": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Global tools", + "title": "Tools" + }, + "toolsets": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Global toolsets", + "title": "Toolsets" + }, + "config": { + "anyOf": [ + { + "$ref": "#/$defs/GlobalConfig" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Global configuration" + }, + "llm": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Default LLM", + "title": "Llm" + }, + "models": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Model configurations", + "title": "Models" + }, + "providers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Provider configurations", + "title": "Providers" + } + }, + "title": "PraisonAI Agents Configuration", + "type": "object" +} diff --git a/src/praisonai/praisonai/config/schema.py b/src/praisonai/praisonai/config/schema.py index ee1dfe23f..d9d01a9a8 100644 --- a/src/praisonai/praisonai/config/schema.py +++ b/src/praisonai/praisonai/config/schema.py @@ -11,6 +11,22 @@ import re +#: Stable, published URL for the ``agents.yaml`` JSON Schema, mirroring the +#: hosting convention used for the CLI-config schema (``config.schema.json``). +#: Editors that speak the YAML language server use this via a leading +#: ``# yaml-language-server: $schema=`` header to provide autocomplete, +#: inline validation, and hover docs while authoring the agent YAML. +AGENTS_SCHEMA_URL = ( + "https://raw.githubusercontent.com/MervinPraison/PraisonAI/main/" + "src/praisonai/praisonai/config/agents.schema.json" +) + +#: Leading YAML comment prepended to scaffolded ``agents.yaml`` files so editors +#: wire up validation out of the box. A leading comment is ignored by +#: ``yaml.safe_load``, so execution is unaffected. +AGENTS_SCHEMA_HEADER = f"# yaml-language-server: $schema={AGENTS_SCHEMA_URL}\n" + + class ProcessType(str, Enum): """Process type for task execution.""" SEQUENTIAL = "sequential" @@ -438,4 +454,80 @@ def format_message(self) -> str: # Resolve forward references for TaskConfig in AgentConfig -AgentConfig.model_rebuild() \ No newline at end of file +AgentConfig.model_rebuild() + + +def _relax_agent_config_for_editor(defs: Dict[str, Any]) -> None: + """Loosen ``AgentConfig`` in ``$defs`` to match the runtime contract. + + The strict :class:`AgentConfig` marks ``role``/``goal``/``backstory`` as + required, but the runtime (``agents_generator._normalize_yaml_config`` and + the adapter canonicalisation step) auto-fills ``role``/``goal`` from the + agent key and maps ``instructions`` -> ``backstory``. Publishing the strict + form would make editors flag valid, executable YAML as invalid, so the + published (authoring) schema drops those ``required`` entries. This only + affects the emitted artefact — the strict model used by ``ConfigValidator`` + is untouched. + """ + agent_def = defs.get("AgentConfig") + if isinstance(agent_def, dict): + agent_def.pop("required", None) + + +def _allow_list_form_agents(schema: Dict[str, Any]) -> None: + """Allow list-form ``roles``/``agents`` in the published schema. + + The runtime accepts both the canonical dict form and a list of named + entries (``agents_generator._list_to_dict``). The dict form remains the + documented default; the list form is added as an alternative so editors + don't reject the list variant. + """ + props = schema.get("properties") + if not isinstance(props, dict): + return + list_form = { + "type": "array", + "items": {"$ref": "#/$defs/AgentConfig"}, + } + for key in ("roles", "agents"): + entry = props.get(key) + if not isinstance(entry, dict): + continue + dict_form = {k: v for k, v in entry.items() if k != "description"} + props[key] = { + "anyOf": [dict_form, list_form], + "description": entry.get("description", ""), + } + + +def generate_agents_schema() -> Dict[str, Any]: + """Generate the JSON Schema for the ``agents.yaml`` file. + + Derived directly from :class:`YAMLConfig` (Pydantic's ``model_json_schema``) + and decorated with the standard ``$schema``/``$id``/``title`` metadata so the + artefact is self-describing and mirrors the CLI-config schema convention. + + The published (authoring) schema is deliberately a touch more permissive + than the strict validator so editors accept every YAML shape the runtime + accepts: list-form ``roles``/``agents`` and configs that rely on runtime + normalisation (``instructions`` -> ``backstory``, auto-filled ``role``/ + ``goal``). The strict :class:`YAMLConfig` used by ``ConfigValidator`` is + left unchanged. + """ + schema = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": AGENTS_SCHEMA_URL, + **YAMLConfig.model_json_schema(), + "title": "PraisonAI Agents Configuration", + "description": ( + "Schema for agents.yaml consumed by the PraisonAI agent runtime " + "(roles/agents, tasks, tools, llm, workflow)." + ), + } + + defs = schema.get("$defs") + if isinstance(defs, dict): + _relax_agent_config_for_editor(defs) + _allow_list_form_agents(schema) + + return schema \ No newline at end of file diff --git a/src/praisonai/pyproject.toml b/src/praisonai/pyproject.toml index d7e7bbd7e..9723057fe 100644 --- a/src/praisonai/pyproject.toml +++ b/src/praisonai/pyproject.toml @@ -316,3 +316,4 @@ exclude = ["praisonai_code*", "praisonai_bot*"] [tool.setuptools.package-data] "praisonai.cli.configuration" = ["*.json"] +"praisonai.config" = ["*.json"] diff --git a/src/praisonai/tests/unit/test_agents_schema_publish.py b/src/praisonai/tests/unit/test_agents_schema_publish.py new file mode 100644 index 000000000..8fc4d0d7e --- /dev/null +++ b/src/praisonai/tests/unit/test_agents_schema_publish.py @@ -0,0 +1,135 @@ +"""Tests for the published agents.yaml JSON Schema and editor autocomplete wiring. + +Covers issue #3427: +- `YAMLConfig.model_json_schema()` is emitted via `generate_agents_schema()`. +- The committed `agents.schema.json` artefact matches the model. +- A scaffolded `agents.yaml` starts with the `# yaml-language-server` header + and still round-trips through `ConfigValidator` (execution unaffected). +""" + +import json +from pathlib import Path + +import pytest +import yaml + +from praisonai.config.schema import ( + AGENTS_SCHEMA_URL, + AGENTS_SCHEMA_HEADER, + generate_agents_schema, +) +from praisonai.config.validator import ConfigValidator + + +def test_generate_agents_schema_derived_from_yamlconfig(): + schema = generate_agents_schema() + assert schema["$schema"] == "http://json-schema.org/draft-07/schema#" + assert schema["$id"] == AGENTS_SCHEMA_URL + props = schema["properties"] + for key in ("roles", "agents", "tasks", "tools", "llm", "workflow"): + assert key in props, f"expected '{key}' in agents schema properties" + + +def test_committed_artefact_matches_model(): + artefact = ( + Path(__file__).resolve().parents[2] + / "praisonai" + / "config" + / "agents.schema.json" + ) + assert artefact.exists(), "agents.schema.json artefact must be committed" + committed = json.loads(artefact.read_text(encoding="utf-8")) + assert committed == generate_agents_schema(), ( + "agents.schema.json is stale; regenerate via " + "`praisonai validate schema -o agents.schema.json`" + ) + + +def test_published_schema_matches_runtime_contract(): + """Editor schema must accept runtime-normalised YAML shapes (issue #3427). + + The runtime (``agents_generator``) auto-fills ``role``/``goal``, maps + ``instructions`` -> ``backstory``, and accepts list-form ``roles``/ + ``agents``. The published schema must not mark those forms invalid, so: + - ``AgentConfig`` has no ``required`` block, and + - ``roles``/``agents`` allow both dict and list forms. + """ + schema = generate_agents_schema() + + agent_def = schema["$defs"]["AgentConfig"] + assert "required" not in agent_def, ( + "published AgentConfig must not force role/goal/backstory; " + "runtime auto-fills / accepts 'instructions'" + ) + + def _collect_types(node): + types = set() + if isinstance(node, dict): + if isinstance(node.get("type"), str): + types.add(node["type"]) + for branch in node.get("anyOf", []): + types |= _collect_types(branch) + return types + + for key in ("roles", "agents"): + types = _collect_types(schema["properties"][key]) + assert "array" in types, ( + f"'{key}' must accept list form (runtime _list_to_dict)" + ) + assert "object" in types, ( + f"'{key}' must still accept canonical dict form" + ) + + +def test_schema_header_points_at_published_url(): + assert AGENTS_SCHEMA_HEADER.startswith("# yaml-language-server: $schema=") + assert AGENTS_SCHEMA_URL in AGENTS_SCHEMA_HEADER + assert AGENTS_SCHEMA_HEADER.endswith("\n") + + +def test_scaffolded_agents_yaml_has_header_and_round_trips(tmp_path): + try: + from praisonai.auto import AutoGenerator + except ImportError: + pytest.skip("AutoGenerator not available") + + agent_file = tmp_path / "agents.yaml" + try: + gen = AutoGenerator( + topic="Research AI trends", + agent_file=str(agent_file), + framework="praisonai", + ) + except ImportError: + pytest.skip("No agent framework adapter available") + + json_data = { + "roles": { + "researcher": { + "role": "Researcher", + "goal": "Research the topic", + "backstory": "Expert researcher.", + "tasks": { + "research": { + "description": "Research AI trends", + "expected_output": "A short report", + } + }, + "tools": [], + } + } + } + gen.convert_and_save(json_data) + + content = agent_file.read_text(encoding="utf-8") + # Header is the very first line so editors pick up the schema. + assert content.startswith("# yaml-language-server: $schema=") + assert AGENTS_SCHEMA_URL in content.splitlines()[0] + + # Leading comment is ignored by safe_load -> execution unaffected. + loaded = yaml.safe_load(content) + assert "roles" in loaded and "researcher" in loaded["roles"] + + # Still round-trips through the runtime validator. + result = ConfigValidator().validate_yaml_string(content) + assert result.valid, result.errors