solid_agent 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +68 -0
  3. data/LICENSE +21 -0
  4. data/README.md +209 -18
  5. data/Rakefile +22 -2
  6. data/docs/agent-md-spec.md +803 -0
  7. data/docs/parser-design.md +1369 -0
  8. data/docs/registry-api.md +882 -0
  9. data/examples/README.md +60 -0
  10. data/examples/manifests/changelog_writer.agent.md +81 -0
  11. data/examples/manifests/usage.rb +96 -0
  12. data/examples/memory_handoff/app/agents/researcher_agent.rb +36 -0
  13. data/examples/memory_handoff/app/agents/writer_agent.rb +41 -0
  14. data/examples/memory_handoff/usage.rb +45 -0
  15. data/examples/persistent_conversation/app/agents/support_agent.rb +59 -0
  16. data/examples/persistent_conversation/app/controllers/support_conversations_controller.rb +24 -0
  17. data/examples/persistent_conversation/app/views/agents/support/instructions.md.erb +8 -0
  18. data/examples/persistent_conversation/usage.rb +51 -0
  19. data/examples/reasoning/app/agents/analysis_agent.rb +52 -0
  20. data/examples/reasoning/usage.rb +52 -0
  21. data/examples/run_tracking/app/agents/report_agent.rb +30 -0
  22. data/examples/run_tracking/app/controllers/agent_runs_controller.rb +43 -0
  23. data/examples/run_tracking/app/jobs/document_analysis_job.rb +17 -0
  24. data/examples/run_tracking/app/services/document_analysis_run.rb +68 -0
  25. data/examples/run_tracking/usage.rb +85 -0
  26. data/examples/tool_streaming/app/agents/browser_agent.rb +65 -0
  27. data/examples/tool_streaming/app/channels/tool_status_channel.rb +24 -0
  28. data/examples/tool_streaming/app/views/browser_agent/tools/fetch_url.json.erb +15 -0
  29. data/examples/tool_streaming/usage.rb +47 -0
  30. data/lib/generators/solid_agent/agent/agent_generator.rb +2 -2
  31. data/lib/generators/solid_agent/agent/templates/agent.rb.erb +3 -3
  32. data/lib/generators/solid_agent/context/templates/context_model.rb.erb +50 -16
  33. data/lib/generators/solid_agent/context/templates/create_generations.rb.erb +8 -0
  34. data/lib/generators/solid_agent/context/templates/create_messages.rb.erb +4 -0
  35. data/lib/generators/solid_agent/context/templates/generation_model.rb.erb +11 -0
  36. data/lib/generators/solid_agent/install/install_generator.rb +9 -0
  37. data/lib/generators/solid_agent/install/templates/agent_context.rb.erb +60 -17
  38. data/lib/generators/solid_agent/install/templates/agent_generation.rb.erb +23 -6
  39. data/lib/generators/solid_agent/install/templates/agent_memory.rb.erb +51 -0
  40. data/lib/generators/solid_agent/install/templates/agent_memory_entry.rb.erb +12 -0
  41. data/lib/generators/solid_agent/install/templates/agent_run.rb.erb +122 -0
  42. data/lib/generators/solid_agent/install/templates/create_agent_generations.rb.erb +13 -0
  43. data/lib/generators/solid_agent/install/templates/create_agent_memories.rb.erb +35 -0
  44. data/lib/generators/solid_agent/install/templates/create_agent_messages.rb.erb +5 -0
  45. data/lib/generators/solid_agent/install/templates/create_agent_runs.rb.erb +46 -0
  46. data/lib/generators/solid_agent/manifest/manifest_generator.rb +209 -0
  47. data/lib/generators/solid_agent/manifest/templates/agent.md.erb +39 -0
  48. data/lib/generators/solid_agent/manifest/templates/prompt.erb +13 -0
  49. data/lib/generators/solid_agent/reasons/reasons_generator.rb +83 -0
  50. data/lib/generators/solid_agent/reasons/templates/add_reasoning_columns.rb.erb +12 -0
  51. data/lib/solid_agent/agent_manifest/agent_builder.rb +323 -0
  52. data/lib/solid_agent/agent_manifest/errors.rb +26 -0
  53. data/lib/solid_agent/agent_manifest/exporter_registry.rb +117 -0
  54. data/lib/solid_agent/agent_manifest/exporters/agent_md_exporter.rb +115 -0
  55. data/lib/solid_agent/agent_manifest/exporters/base_exporter.rb +152 -0
  56. data/lib/solid_agent/agent_manifest/exporters/crewai_exporter.rb +125 -0
  57. data/lib/solid_agent/agent_manifest/exporters/dotprompt_exporter.rb +92 -0
  58. data/lib/solid_agent/agent_manifest/input_schema.rb +154 -0
  59. data/lib/solid_agent/agent_manifest/manifest.rb +306 -0
  60. data/lib/solid_agent/agent_manifest/parser_registry.rb +185 -0
  61. data/lib/solid_agent/agent_manifest/parsers/agent_md_parser.rb +87 -0
  62. data/lib/solid_agent/agent_manifest/parsers/base_parser.rb +223 -0
  63. data/lib/solid_agent/agent_manifest/parsers/crewai_parser.rb +201 -0
  64. data/lib/solid_agent/agent_manifest/parsers/dotprompt_parser.rb +122 -0
  65. data/lib/solid_agent/agent_manifest/parsers/github_prompt_parser.rb +143 -0
  66. data/lib/solid_agent/agent_manifest/picoschema.rb +254 -0
  67. data/lib/solid_agent/agent_manifest/registry/auth.rb +103 -0
  68. data/lib/solid_agent/agent_manifest/registry/client.rb +384 -0
  69. data/lib/solid_agent/agent_manifest/resource.rb +103 -0
  70. data/lib/solid_agent/agent_manifest/tool.rb +160 -0
  71. data/lib/solid_agent/agent_manifest/validator.rb +368 -0
  72. data/lib/solid_agent/agent_manifest.rb +381 -0
  73. data/lib/solid_agent/has_context.rb +251 -30
  74. data/lib/solid_agent/has_memory.rb +136 -0
  75. data/lib/solid_agent/has_reasons.rb +230 -0
  76. data/lib/solid_agent/model_naming.rb +42 -0
  77. data/lib/solid_agent/model_pricing.rb +93 -0
  78. data/lib/solid_agent/reasonable/reason.rb +205 -0
  79. data/lib/solid_agent/reasonable.rb +181 -0
  80. data/lib/solid_agent/records/agent.rb +520 -0
  81. data/lib/solid_agent/records/agent_run.rb +520 -0
  82. data/lib/solid_agent/records/agent_template.rb +142 -0
  83. data/lib/solid_agent/records/agent_version.rb +141 -0
  84. data/lib/solid_agent/records/ownable.rb +130 -0
  85. data/lib/solid_agent/records.rb +152 -0
  86. data/lib/solid_agent/run_fingerprint.rb +51 -0
  87. data/lib/solid_agent/tool_cache.rb +91 -0
  88. data/lib/solid_agent/version.rb +1 -1
  89. data/lib/solid_agent.rb +70 -3
  90. metadata +87 -1
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidAgent
4
+ module AgentManifest
5
+ module Exporters
6
+ # AgentMdExporter converts Manifests to the native .agent.md format.
7
+ #
8
+ # @example Export a manifest
9
+ # content = AgentMdExporter.export(manifest)
10
+ #
11
+ # @example Export with options
12
+ # content = AgentMdExporter.export(manifest, include_examples: true)
13
+ #
14
+ class AgentMdExporter < BaseExporter
15
+ class << self
16
+ def format_name
17
+ :agent_md
18
+ end
19
+
20
+ # Export manifest to .agent.md format
21
+ #
22
+ # @param manifest [Manifest] The manifest to export
23
+ # @param include_examples [Boolean] Include examples section
24
+ # @param include_tests [Boolean] Include tests section
25
+ # @return [String] Exported content
26
+ def export(manifest, include_examples: false, include_tests: false, **)
27
+ frontmatter = build_agent_md_frontmatter(manifest, include_examples, include_tests)
28
+ body = build_body(manifest)
29
+
30
+ "#{frontmatter}\n#{body}"
31
+ end
32
+
33
+ private
34
+
35
+ def build_agent_md_frontmatter(manifest, include_examples, include_tests)
36
+ data = {}
37
+
38
+ # Meta
39
+ data["name"] = manifest.name
40
+ data["version"] = manifest.version if manifest.version && manifest.version != "1.0.0"
41
+ data["description"] = manifest.description
42
+ data["author"] = manifest.author
43
+ data["license"] = manifest.license
44
+ data["repository"] = manifest.repository
45
+ data["tags"] = manifest.tags if manifest.tags&.any?
46
+ data["extends"] = manifest.extends
47
+
48
+ # Model
49
+ data["model"] = manifest.model
50
+ data["config"] = manifest.config if manifest.config&.any?
51
+
52
+ # Schemas
53
+ data["input"] = export_input_schema(manifest.input_schema)
54
+ data["output"] = manifest.output_schema
55
+
56
+ # Tools & Resources
57
+ data["tools"] = export_tools(manifest.tools)
58
+ data["resources"] = export_resources(manifest.resources)
59
+
60
+ # Extensions (preserve framework-specific config)
61
+ manifest.extensions&.each do |framework, config|
62
+ data[framework.to_s] = config if config&.any?
63
+ end
64
+
65
+ # Examples & Tests
66
+ data["examples"] = manifest.examples if include_examples && manifest.examples&.any?
67
+ data["tests"] = manifest.tests if include_tests && manifest.tests&.any?
68
+
69
+ build_frontmatter(data)
70
+ end
71
+
72
+ def build_body(manifest)
73
+ parts = []
74
+
75
+ # Title from name
76
+ title = manifest.name.to_s.tr("-", " ").split.map(&:capitalize).join(" ")
77
+ parts << "# #{title}\n"
78
+
79
+ # Description
80
+ if manifest.description.present?
81
+ parts << manifest.description
82
+ parts << ""
83
+ end
84
+
85
+ # Instructions section
86
+ if manifest.instructions.present? && manifest.instructions != manifest.template
87
+ parts << "## Instructions\n"
88
+ parts << manifest.instructions
89
+ parts << ""
90
+ end
91
+
92
+ # Template (if different from instructions or if instructions is nil)
93
+ if manifest.template.present?
94
+ # Check if template is just the full body or needs a section
95
+ if manifest.instructions.blank?
96
+ # Template is the main content
97
+ parts << manifest.template
98
+ elsif manifest.template != manifest.instructions
99
+ parts << "## Template\n"
100
+ parts << "```liquid"
101
+ parts << manifest.template
102
+ parts << "```"
103
+ end
104
+ end
105
+
106
+ parts.join("\n").strip + "\n"
107
+ end
108
+ end
109
+ end
110
+
111
+ # Register the exporter
112
+ ExporterRegistry.register(:agent_md, AgentMdExporter)
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidAgent
4
+ module AgentManifest
5
+ module Exporters
6
+ # BaseExporter provides common functionality for all format exporters.
7
+ #
8
+ # Subclasses should implement:
9
+ # - .export(manifest, **options) - Convert manifest to string
10
+ # - .format_name - Return the format identifier symbol
11
+ #
12
+ class BaseExporter
13
+ class << self
14
+ # Export a manifest to string format
15
+ #
16
+ # @param manifest [Manifest] The manifest to export
17
+ # @param options [Hash] Format-specific options
18
+ # @return [String] Exported content
19
+ def export(manifest, **options)
20
+ raise NotImplementedError, "#{self}.export must be implemented by subclass"
21
+ end
22
+
23
+ # Get the format name for this exporter
24
+ #
25
+ # @return [Symbol]
26
+ def format_name
27
+ raise NotImplementedError, "#{self}.format_name must be implemented by subclass"
28
+ end
29
+
30
+ protected
31
+
32
+ # Build YAML frontmatter from a hash
33
+ #
34
+ # @param data [Hash] Frontmatter data
35
+ # @return [String] YAML frontmatter block
36
+ def build_frontmatter(data)
37
+ # Remove nil values and empty hashes/arrays
38
+ clean_data = deep_compact(data)
39
+ return "" if clean_data.empty?
40
+
41
+ yaml_content = clean_data.to_yaml
42
+ # Remove the leading "---\n" that to_yaml adds
43
+ yaml_content = yaml_content.sub(/\A---\n/, "")
44
+
45
+ "---\n#{yaml_content}---\n"
46
+ end
47
+
48
+ # Deep compact - remove nil values and empty collections recursively
49
+ #
50
+ # @param obj [Object] Object to compact
51
+ # @return [Object] Compacted object
52
+ def deep_compact(obj)
53
+ case obj
54
+ when Hash
55
+ result = {}
56
+ obj.each do |k, v|
57
+ compacted = deep_compact(v)
58
+ result[k] = compacted unless empty_value?(compacted)
59
+ end
60
+ result
61
+ when Array
62
+ obj.map { |v| deep_compact(v) }.reject { |v| empty_value?(v) }
63
+ else
64
+ obj
65
+ end
66
+ end
67
+
68
+ # Check if a value should be considered empty
69
+ #
70
+ # @param value [Object]
71
+ # @return [Boolean]
72
+ def empty_value?(value)
73
+ case value
74
+ when nil then true
75
+ when Hash then value.empty?
76
+ when Array then value.empty?
77
+ when String then value.empty?
78
+ else false
79
+ end
80
+ end
81
+
82
+ # Convert tools to exportable format
83
+ #
84
+ # @param tools [Array<Tool>] Tools to convert
85
+ # @return [Array<Hash>] Exportable tool data
86
+ def export_tools(tools)
87
+ return nil if tools.nil? || tools.empty?
88
+
89
+ tools.map do |tool|
90
+ if tool.reference?
91
+ { "$ref" => tool.ref }
92
+ else
93
+ {
94
+ "name" => tool.name,
95
+ "description" => tool.description,
96
+ "inputSchema" => tool.input_schema
97
+ }.compact
98
+ end
99
+ end
100
+ end
101
+
102
+ # Convert resources to exportable format
103
+ #
104
+ # @param resources [Array<Resource>] Resources to convert
105
+ # @return [Array<Hash>] Exportable resource data
106
+ def export_resources(resources)
107
+ return nil if resources.nil? || resources.empty?
108
+
109
+ resources.map do |resource|
110
+ {
111
+ "name" => resource.name,
112
+ "description" => resource.description,
113
+ "uri" => resource.uri,
114
+ "mimeType" => resource.mime_type
115
+ }.compact
116
+ end
117
+ end
118
+
119
+ # Convert input schema to exportable format
120
+ #
121
+ # @param schema [InputSchema, nil] Schema to convert
122
+ # @return [Hash, nil] Exportable schema data
123
+ def export_input_schema(schema)
124
+ return nil unless schema
125
+
126
+ { "schema" => schema.schema }
127
+ end
128
+
129
+ # Strip provider prefix from model identifier
130
+ #
131
+ # @param model [String, nil] Full model identifier
132
+ # @return [String, nil] Model name without provider
133
+ def strip_provider(model)
134
+ return nil unless model
135
+ model.to_s.split("/").last
136
+ end
137
+
138
+ # Wrap text at specified width
139
+ #
140
+ # @param text [String] Text to wrap
141
+ # @param width [Integer] Maximum line width
142
+ # @return [String] Wrapped text
143
+ def word_wrap(text, width: 80)
144
+ return text unless text
145
+
146
+ text.gsub(/(.{1,#{width}})(\s+|$)/, "\\1\n").strip
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidAgent
4
+ module AgentManifest
5
+ module Exporters
6
+ # CrewAIExporter converts Manifests to CrewAI's agents.yaml format.
7
+ #
8
+ # @see https://docs.crewai.com
9
+ #
10
+ # @example Export a single manifest
11
+ # content = CrewAIExporter.export(manifest)
12
+ #
13
+ # @example Export multiple manifests
14
+ # content = CrewAIExporter.export_multiple([manifest1, manifest2])
15
+ #
16
+ class CrewAIExporter < BaseExporter
17
+ class << self
18
+ def format_name
19
+ :crewai
20
+ end
21
+
22
+ # Export manifest to CrewAI agents.yaml format
23
+ #
24
+ # @param manifest [Manifest] The manifest to export
25
+ # @return [String] Exported YAML content
26
+ def export(manifest, **)
27
+ export_multiple([manifest])
28
+ end
29
+
30
+ # Export multiple manifests to a single agents.yaml
31
+ #
32
+ # @param manifests [Array<Manifest>] Manifests to export
33
+ # @return [String] Exported YAML content
34
+ def export_multiple(manifests)
35
+ agents = {}
36
+
37
+ manifests.each do |manifest|
38
+ agent_key = manifest.name.to_s.tr("-", "_")
39
+ agents[agent_key] = build_crewai_agent(manifest)
40
+ end
41
+
42
+ # Use YAML dump with custom options for better formatting
43
+ yaml = agents.to_yaml
44
+ # Remove leading ---
45
+ yaml.sub(/\A---\n/, "")
46
+ end
47
+
48
+ private
49
+
50
+ def build_crewai_agent(manifest)
51
+ agent = {}
52
+
53
+ # Extract from CrewAI extensions if present
54
+ crewai_ext = manifest.extensions&.dig(:crewai) || {}
55
+
56
+ # Role - from extension or generate from name
57
+ agent["role"] = crewai_ext[:role] || crewai_ext["role"] ||
58
+ manifest.name.to_s.tr("-", " ").split.map(&:capitalize).join(" ")
59
+
60
+ # Goal - from extension or description
61
+ agent["goal"] = crewai_ext[:goal] || crewai_ext["goal"] || manifest.description
62
+
63
+ # Backstory - from extension or instructions
64
+ backstory = crewai_ext[:backstory] || crewai_ext["backstory"]
65
+ if backstory.blank? && manifest.instructions.present?
66
+ # Extract backstory from instructions if it follows the Role/Goal/Backstory format
67
+ backstory = extract_backstory(manifest.instructions)
68
+ end
69
+ agent["backstory"] = backstory if backstory.present?
70
+
71
+ # LLM - strip provider prefix for CrewAI
72
+ agent["llm"] = strip_provider(manifest.model) if manifest.model.present?
73
+
74
+ # Tools - CrewAI uses Python class names
75
+ if manifest.tools&.any?
76
+ agent["tools"] = manifest.tools.map do |tool|
77
+ if tool.reference?
78
+ # Extract tool name from reference
79
+ tool.ref.to_s.split("://").last.split("/").last
80
+ else
81
+ # Convert to PascalCase tool class name
82
+ tool.name.to_s.split(/[-_]/).map(&:capitalize).join + "Tool"
83
+ end
84
+ end
85
+ end
86
+
87
+ # CrewAI-specific options from extensions
88
+ %w[verbose allow_delegation max_iter max_rpm memory cache].each do |key|
89
+ value = crewai_ext[key.to_sym] || crewai_ext[key]
90
+ agent[key] = value unless value.nil?
91
+ end
92
+
93
+ # Config options
94
+ if manifest.config&.any?
95
+ agent["temperature"] = manifest.config[:temperature] || manifest.config["temperature"]
96
+ agent["max_tokens"] = manifest.config[:max_tokens] || manifest.config["max_tokens"]
97
+ end
98
+
99
+ agent.compact
100
+ end
101
+
102
+ # Extract backstory from instructions that follow Role/Goal/Backstory format
103
+ def extract_backstory(instructions)
104
+ return nil unless instructions
105
+
106
+ # Look for content after "Backstory:" or similar patterns
107
+ if instructions =~ /backstory:?\s*(.+?)(?:\n\n|\z)/mi
108
+ return ::Regexp.last_match(1).strip
109
+ end
110
+
111
+ # If instructions don't have Role/Goal prefix, use the whole thing as backstory
112
+ unless instructions =~ /\A\s*(Role:|Goal:)/i
113
+ return instructions
114
+ end
115
+
116
+ nil
117
+ end
118
+ end
119
+ end
120
+
121
+ # Register the exporter
122
+ ExporterRegistry.register(:crewai, CrewAIExporter)
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidAgent
4
+ module AgentManifest
5
+ module Exporters
6
+ # DotpromptExporter converts Manifests to Google Genkit's .prompt format.
7
+ #
8
+ # @see https://github.com/google/dotprompt
9
+ #
10
+ # @example Export a manifest
11
+ # content = DotpromptExporter.export(manifest)
12
+ #
13
+ class DotpromptExporter < BaseExporter
14
+ class << self
15
+ def format_name
16
+ :dotprompt
17
+ end
18
+
19
+ # Export manifest to .prompt format
20
+ #
21
+ # @param manifest [Manifest] The manifest to export
22
+ # @param use_picoschema [Boolean] Export input schema as Picoschema
23
+ # @return [String] Exported content
24
+ def export(manifest, use_picoschema: true, **)
25
+ frontmatter = build_dotprompt_frontmatter(manifest, use_picoschema)
26
+ body = manifest.template || manifest.instructions || ""
27
+
28
+ "#{frontmatter}\n#{body.strip}\n"
29
+ end
30
+
31
+ private
32
+
33
+ def build_dotprompt_frontmatter(manifest, use_picoschema)
34
+ data = {}
35
+
36
+ # Basic fields
37
+ data["name"] = manifest.name if manifest.name.present?
38
+ data["model"] = manifest.model
39
+ data["description"] = manifest.description
40
+
41
+ # Config fields - Dotprompt uses camelCase
42
+ if manifest.config&.any?
43
+ data["temperature"] = manifest.config[:temperature] || manifest.config["temperature"]
44
+ data["maxOutputTokens"] = manifest.config[:max_tokens] || manifest.config["max_tokens"]
45
+ data["topP"] = manifest.config[:top_p] || manifest.config["top_p"]
46
+ data["topK"] = manifest.config[:top_k] || manifest.config["top_k"]
47
+ data["stopSequences"] = manifest.config[:stop] || manifest.config["stop"]
48
+ end
49
+
50
+ # Input schema
51
+ if manifest.input_schema
52
+ input_data = {}
53
+ if use_picoschema
54
+ input_data["schema"] = manifest.input_schema.to_picoschema
55
+ else
56
+ input_data["schema"] = manifest.input_schema.to_json_schema
57
+ end
58
+ data["input"] = input_data
59
+ end
60
+
61
+ # Output schema
62
+ if manifest.output_schema
63
+ data["output"] = manifest.output_schema
64
+ end
65
+
66
+ # Tools - Dotprompt format
67
+ if manifest.tools&.any?
68
+ data["tools"] = manifest.tools.map do |tool|
69
+ if tool.reference?
70
+ tool.ref
71
+ else
72
+ tool.name
73
+ end
74
+ end
75
+ end
76
+
77
+ # Preserve dotprompt-specific extensions
78
+ dotprompt_ext = manifest.extensions&.dig(:dotprompt) || {}
79
+ %w[candidates cache default variant metadata].each do |key|
80
+ data[key] = dotprompt_ext[key] if dotprompt_ext[key].present?
81
+ end
82
+
83
+ build_frontmatter(data)
84
+ end
85
+ end
86
+ end
87
+
88
+ # Register the exporter
89
+ ExporterRegistry.register(:dotprompt, DotpromptExporter)
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidAgent
4
+ module AgentManifest
5
+ # InputSchema wraps a schema definition, supporting both Picoschema
6
+ # (compact YAML format) and JSON Schema formats.
7
+ #
8
+ # Provides bidirectional conversion between formats for interoperability.
9
+ #
10
+ # @example Picoschema format
11
+ # schema = InputSchema.new(
12
+ # schema: {
13
+ # "query" => "string, the search query",
14
+ # "limit?" => "integer, max results"
15
+ # },
16
+ # format: :picoschema
17
+ # )
18
+ # schema.to_json_schema # => JSON Schema object
19
+ #
20
+ # @example JSON Schema format
21
+ # schema = InputSchema.new(
22
+ # schema: {
23
+ # "type" => "object",
24
+ # "properties" => { "query" => { "type" => "string" } }
25
+ # },
26
+ # format: :json_schema
27
+ # )
28
+ # schema.to_picoschema # => Picoschema object
29
+ #
30
+ class InputSchema
31
+ # @return [Hash] The schema definition
32
+ attr_accessor :schema
33
+
34
+ # @return [Symbol] Schema format (:picoschema or :json_schema)
35
+ attr_accessor :format
36
+
37
+ def initialize(schema:, format: :picoschema)
38
+ @schema = schema
39
+ @format = format.to_sym
40
+ end
41
+
42
+ # Convert schema to JSON Schema format
43
+ #
44
+ # @return [Hash] JSON Schema object
45
+ def to_json_schema
46
+ case format
47
+ when :picoschema
48
+ Picoschema.to_json_schema(schema)
49
+ when :json_schema
50
+ schema
51
+ else
52
+ raise SchemaError, "Unknown schema format: #{format}"
53
+ end
54
+ end
55
+
56
+ # Convert schema to Picoschema format
57
+ #
58
+ # @return [Hash] Picoschema object
59
+ def to_picoschema
60
+ case format
61
+ when :picoschema
62
+ schema
63
+ when :json_schema
64
+ Picoschema.from_json_schema(schema)
65
+ else
66
+ raise SchemaError, "Unknown schema format: #{format}"
67
+ end
68
+ end
69
+
70
+ # Convert to hash representation
71
+ #
72
+ # @return [Hash]
73
+ def to_h
74
+ {
75
+ schema: schema,
76
+ format: format
77
+ }
78
+ end
79
+
80
+ # Validate input data against this schema
81
+ #
82
+ # Requires the json_schemer gem for validation.
83
+ #
84
+ # @param input_hash [Hash] Input data to validate
85
+ # @return [Array<String>] List of validation errors (empty if valid)
86
+ def validate_input(input_hash)
87
+ return [] unless defined?(JSONSchemer)
88
+
89
+ json_schema = to_json_schema
90
+ schemer = JSONSchemer.schema(json_schema)
91
+ schemer.validate(input_hash).map { |error| error["error"] }
92
+ rescue => e
93
+ ["Schema validation error: #{e.message}"]
94
+ end
95
+
96
+ # Check if input is valid against schema
97
+ #
98
+ # @param input_hash [Hash] Input data to validate
99
+ # @return [Boolean]
100
+ def valid_input?(input_hash)
101
+ validate_input(input_hash).empty?
102
+ end
103
+
104
+ # Get list of required field names
105
+ #
106
+ # @return [Array<String>]
107
+ def required_fields
108
+ json = to_json_schema
109
+ json["required"] || []
110
+ end
111
+
112
+ # Get all field names
113
+ #
114
+ # @return [Array<String>]
115
+ def field_names
116
+ json = to_json_schema
117
+ (json["properties"] || {}).keys
118
+ end
119
+
120
+ # Get field definition
121
+ #
122
+ # @param field_name [String] Name of the field
123
+ # @return [Hash, nil] Field schema definition
124
+ def field(field_name)
125
+ json = to_json_schema
126
+ json.dig("properties", field_name.to_s)
127
+ end
128
+
129
+ # Create from various input formats
130
+ #
131
+ # @param data [Hash] Schema data
132
+ # @return [InputSchema]
133
+ def self.from_hash(data)
134
+ schema = data["schema"] || data[:schema]
135
+ format = data["format"] || data[:format] || detect_format(schema)
136
+
137
+ new(schema: schema, format: format)
138
+ end
139
+
140
+ # Detect schema format from content
141
+ #
142
+ # @param schema [Hash] Schema to analyze
143
+ # @return [Symbol] Detected format
144
+ def self.detect_format(schema)
145
+ return :json_schema if schema.nil?
146
+ return :json_schema if schema["type"].present?
147
+ return :json_schema if schema["properties"].present?
148
+ return :json_schema if schema["$schema"].present?
149
+
150
+ :picoschema
151
+ end
152
+ end
153
+ end
154
+ end