solid_agent 0.0.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +68 -0
- data/LICENSE +21 -0
- data/README.md +321 -0
- data/Rakefile +32 -0
- data/docs/agent-md-spec.md +803 -0
- data/docs/parser-design.md +1369 -0
- data/docs/registry-api.md +882 -0
- data/examples/README.md +60 -0
- data/examples/manifests/changelog_writer.agent.md +81 -0
- data/examples/manifests/usage.rb +96 -0
- data/examples/memory_handoff/app/agents/researcher_agent.rb +36 -0
- data/examples/memory_handoff/app/agents/writer_agent.rb +41 -0
- data/examples/memory_handoff/usage.rb +45 -0
- data/examples/persistent_conversation/app/agents/support_agent.rb +59 -0
- data/examples/persistent_conversation/app/controllers/support_conversations_controller.rb +24 -0
- data/examples/persistent_conversation/app/views/agents/support/instructions.md.erb +8 -0
- data/examples/persistent_conversation/usage.rb +51 -0
- data/examples/reasoning/app/agents/analysis_agent.rb +52 -0
- data/examples/reasoning/usage.rb +52 -0
- data/examples/run_tracking/app/agents/report_agent.rb +30 -0
- data/examples/run_tracking/app/controllers/agent_runs_controller.rb +43 -0
- data/examples/run_tracking/app/jobs/document_analysis_job.rb +17 -0
- data/examples/run_tracking/app/services/document_analysis_run.rb +68 -0
- data/examples/run_tracking/usage.rb +85 -0
- data/examples/tool_streaming/app/agents/browser_agent.rb +65 -0
- data/examples/tool_streaming/app/channels/tool_status_channel.rb +24 -0
- data/examples/tool_streaming/app/views/browser_agent/tools/fetch_url.json.erb +15 -0
- data/examples/tool_streaming/usage.rb +47 -0
- data/lib/generators/solid_agent/agent/agent_generator.rb +95 -0
- data/lib/generators/solid_agent/agent/templates/action.text.erb +10 -0
- data/lib/generators/solid_agent/agent/templates/agent.rb.erb +93 -0
- data/lib/generators/solid_agent/context/context_generator.rb +124 -0
- data/lib/generators/solid_agent/context/templates/context_model.rb.erb +134 -0
- data/lib/generators/solid_agent/context/templates/create_context.rb.erb +32 -0
- data/lib/generators/solid_agent/context/templates/create_generations.rb.erb +46 -0
- data/lib/generators/solid_agent/context/templates/create_messages.rb.erb +37 -0
- data/lib/generators/solid_agent/context/templates/generation_model.rb.erb +51 -0
- data/lib/generators/solid_agent/context/templates/message_model.rb.erb +47 -0
- data/lib/generators/solid_agent/install/install_generator.rb +92 -0
- data/lib/generators/solid_agent/install/templates/agent_context.rb.erb +171 -0
- data/lib/generators/solid_agent/install/templates/agent_generation.rb.erb +76 -0
- data/lib/generators/solid_agent/install/templates/agent_memory.rb.erb +51 -0
- data/lib/generators/solid_agent/install/templates/agent_memory_entry.rb.erb +12 -0
- data/lib/generators/solid_agent/install/templates/agent_message.rb.erb +76 -0
- data/lib/generators/solid_agent/install/templates/agent_run.rb.erb +122 -0
- data/lib/generators/solid_agent/install/templates/create_agent_contexts.rb.erb +32 -0
- data/lib/generators/solid_agent/install/templates/create_agent_generations.rb.erb +51 -0
- data/lib/generators/solid_agent/install/templates/create_agent_memories.rb.erb +35 -0
- data/lib/generators/solid_agent/install/templates/create_agent_messages.rb.erb +38 -0
- data/lib/generators/solid_agent/install/templates/create_agent_runs.rb.erb +46 -0
- data/lib/generators/solid_agent/install/templates/initializer.rb.erb +51 -0
- data/lib/generators/solid_agent/manifest/manifest_generator.rb +209 -0
- data/lib/generators/solid_agent/manifest/templates/agent.md.erb +39 -0
- data/lib/generators/solid_agent/manifest/templates/prompt.erb +13 -0
- data/lib/generators/solid_agent/reasons/reasons_generator.rb +83 -0
- data/lib/generators/solid_agent/reasons/templates/add_reasoning_columns.rb.erb +12 -0
- data/lib/generators/solid_agent/tool/templates/tool.json.erb +19 -0
- data/lib/generators/solid_agent/tool/tool_generator.rb +117 -0
- data/lib/solid_agent/agent_manifest/agent_builder.rb +323 -0
- data/lib/solid_agent/agent_manifest/errors.rb +26 -0
- data/lib/solid_agent/agent_manifest/exporter_registry.rb +117 -0
- data/lib/solid_agent/agent_manifest/exporters/agent_md_exporter.rb +115 -0
- data/lib/solid_agent/agent_manifest/exporters/base_exporter.rb +152 -0
- data/lib/solid_agent/agent_manifest/exporters/crewai_exporter.rb +125 -0
- data/lib/solid_agent/agent_manifest/exporters/dotprompt_exporter.rb +92 -0
- data/lib/solid_agent/agent_manifest/input_schema.rb +154 -0
- data/lib/solid_agent/agent_manifest/manifest.rb +306 -0
- data/lib/solid_agent/agent_manifest/parser_registry.rb +185 -0
- data/lib/solid_agent/agent_manifest/parsers/agent_md_parser.rb +87 -0
- data/lib/solid_agent/agent_manifest/parsers/base_parser.rb +223 -0
- data/lib/solid_agent/agent_manifest/parsers/crewai_parser.rb +201 -0
- data/lib/solid_agent/agent_manifest/parsers/dotprompt_parser.rb +122 -0
- data/lib/solid_agent/agent_manifest/parsers/github_prompt_parser.rb +143 -0
- data/lib/solid_agent/agent_manifest/picoschema.rb +254 -0
- data/lib/solid_agent/agent_manifest/registry/auth.rb +103 -0
- data/lib/solid_agent/agent_manifest/registry/client.rb +384 -0
- data/lib/solid_agent/agent_manifest/resource.rb +103 -0
- data/lib/solid_agent/agent_manifest/tool.rb +160 -0
- data/lib/solid_agent/agent_manifest/validator.rb +368 -0
- data/lib/solid_agent/agent_manifest.rb +381 -0
- data/lib/solid_agent/engine.rb +16 -0
- data/lib/solid_agent/has_context.rb +670 -0
- data/lib/solid_agent/has_memory.rb +136 -0
- data/lib/solid_agent/has_reasons.rb +230 -0
- data/lib/solid_agent/has_tools.rb +257 -0
- data/lib/solid_agent/model_naming.rb +42 -0
- data/lib/solid_agent/model_pricing.rb +93 -0
- data/lib/solid_agent/reasonable/reason.rb +205 -0
- data/lib/solid_agent/reasonable.rb +181 -0
- data/lib/solid_agent/records/agent.rb +520 -0
- data/lib/solid_agent/records/agent_run.rb +520 -0
- data/lib/solid_agent/records/agent_template.rb +142 -0
- data/lib/solid_agent/records/agent_version.rb +141 -0
- data/lib/solid_agent/records/ownable.rb +130 -0
- data/lib/solid_agent/records.rb +152 -0
- data/lib/solid_agent/run_fingerprint.rb +51 -0
- data/lib/solid_agent/streams_tool_updates.rb +178 -0
- data/lib/solid_agent/tool_cache.rb +91 -0
- data/lib/solid_agent/version.rb +5 -0
- data/lib/solid_agent.rb +95 -0
- data/sig/solid_agent.rbs +4 -0
- metadata +174 -14
|
@@ -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
|