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,223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidAgent
|
|
4
|
+
module AgentManifest
|
|
5
|
+
module Parsers
|
|
6
|
+
# BaseParser provides common functionality for all format parsers.
|
|
7
|
+
#
|
|
8
|
+
# Subclasses should implement:
|
|
9
|
+
# - .parse_string(content) - Parse content string into a Manifest
|
|
10
|
+
# - .format_name - Return the format identifier symbol
|
|
11
|
+
#
|
|
12
|
+
class BaseParser
|
|
13
|
+
FRONTMATTER_REGEX = /\A---\s*\n(.*?)\n---\s*\n(.*)\z/m
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
# Parse a file into a Manifest
|
|
17
|
+
#
|
|
18
|
+
# @param path [String] Path to the file
|
|
19
|
+
# @return [Manifest] Parsed manifest
|
|
20
|
+
def parse(path)
|
|
21
|
+
content = File.read(path, encoding: "UTF-8")
|
|
22
|
+
manifest = parse_string(content)
|
|
23
|
+
|
|
24
|
+
# Handle array results (e.g., CrewAI with multiple agents)
|
|
25
|
+
if manifest.is_a?(Array)
|
|
26
|
+
manifest.each do |m|
|
|
27
|
+
m.source_path = path
|
|
28
|
+
m.source_format = format_name
|
|
29
|
+
end
|
|
30
|
+
else
|
|
31
|
+
manifest.source_path = path
|
|
32
|
+
manifest.source_format = format_name
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
manifest
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Parse content string into a Manifest
|
|
39
|
+
#
|
|
40
|
+
# @param content [String] File content
|
|
41
|
+
# @return [Manifest] Parsed manifest
|
|
42
|
+
def parse_string(content)
|
|
43
|
+
raise NotImplementedError, "#{self}.parse_string must be implemented by subclass"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get the format name for this parser
|
|
47
|
+
#
|
|
48
|
+
# @return [Symbol]
|
|
49
|
+
def format_name
|
|
50
|
+
raise NotImplementedError, "#{self}.format_name must be implemented by subclass"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
protected
|
|
54
|
+
|
|
55
|
+
# Extract YAML frontmatter from markdown content
|
|
56
|
+
#
|
|
57
|
+
# @param content [String] Full content with frontmatter
|
|
58
|
+
# @return [Array<Hash, String>] [frontmatter_hash, body]
|
|
59
|
+
def extract_frontmatter(content)
|
|
60
|
+
return [{}, content.strip] if content.blank?
|
|
61
|
+
|
|
62
|
+
match = content.match(FRONTMATTER_REGEX)
|
|
63
|
+
|
|
64
|
+
unless match
|
|
65
|
+
# Check if frontmatter was intended but malformed
|
|
66
|
+
if content.strip.start_with?("---")
|
|
67
|
+
raise ParseError, "Malformed frontmatter: missing closing '---' delimiter"
|
|
68
|
+
end
|
|
69
|
+
return [{}, content.strip]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
begin
|
|
73
|
+
frontmatter = YAML.safe_load(
|
|
74
|
+
match[1],
|
|
75
|
+
permitted_classes: [Symbol, Date, Time],
|
|
76
|
+
permitted_symbols: [],
|
|
77
|
+
aliases: false
|
|
78
|
+
)
|
|
79
|
+
rescue Psych::SyntaxError => e
|
|
80
|
+
raise ParseError, "Invalid YAML in frontmatter: #{e.message}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
[frontmatter || {}, match[2].strip]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Normalize model identifier to provider/model format
|
|
87
|
+
#
|
|
88
|
+
# @param model_string [String, nil] Model identifier
|
|
89
|
+
# @return [String, nil] Normalized model identifier
|
|
90
|
+
def normalize_model(model_string)
|
|
91
|
+
return nil unless model_string.present?
|
|
92
|
+
|
|
93
|
+
model_string = model_string.to_s.strip
|
|
94
|
+
|
|
95
|
+
# Already in provider/model format
|
|
96
|
+
return model_string if model_string.include?("/")
|
|
97
|
+
|
|
98
|
+
# Detect provider from model name
|
|
99
|
+
if model_string.start_with?("gpt-") || model_string.start_with?("o1") || model_string.start_with?("o3")
|
|
100
|
+
"openai/#{model_string}"
|
|
101
|
+
elsif model_string.start_with?("claude-")
|
|
102
|
+
"anthropic/#{model_string}"
|
|
103
|
+
elsif model_string.start_with?("gemini-")
|
|
104
|
+
"google/#{model_string}"
|
|
105
|
+
elsif model_string.start_with?("llama") || model_string.start_with?("mixtral")
|
|
106
|
+
"meta/#{model_string}"
|
|
107
|
+
else
|
|
108
|
+
# Unknown - return as-is
|
|
109
|
+
model_string
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Parse tools array from frontmatter data
|
|
114
|
+
#
|
|
115
|
+
# @param tools_data [Array, nil] Tools data from frontmatter
|
|
116
|
+
# @return [Array<Tool>] Parsed tools
|
|
117
|
+
def parse_tools(tools_data)
|
|
118
|
+
return [] unless tools_data.is_a?(Array)
|
|
119
|
+
|
|
120
|
+
tools_data.map do |tool_data|
|
|
121
|
+
if tool_data.is_a?(String)
|
|
122
|
+
# String reference
|
|
123
|
+
Tool.new(ref: tool_data)
|
|
124
|
+
elsif tool_data["$ref"]
|
|
125
|
+
# Explicit reference
|
|
126
|
+
Tool.new(ref: tool_data["$ref"])
|
|
127
|
+
else
|
|
128
|
+
# Inline definition
|
|
129
|
+
Tool.from_hash(tool_data)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Parse resources array from frontmatter data
|
|
135
|
+
#
|
|
136
|
+
# @param resources_data [Array, nil] Resources data from frontmatter
|
|
137
|
+
# @return [Array<Resource>] Parsed resources
|
|
138
|
+
def parse_resources(resources_data)
|
|
139
|
+
return [] unless resources_data.is_a?(Array)
|
|
140
|
+
|
|
141
|
+
resources_data.map { |data| Resource.from_hash(data) }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Parse input schema from frontmatter data
|
|
145
|
+
#
|
|
146
|
+
# @param input_data [Hash, nil] Input schema data
|
|
147
|
+
# @return [InputSchema, nil] Parsed input schema
|
|
148
|
+
def parse_input_schema(input_data)
|
|
149
|
+
return nil unless input_data.is_a?(Hash)
|
|
150
|
+
|
|
151
|
+
schema = input_data["schema"] || input_data[:schema]
|
|
152
|
+
return nil unless schema
|
|
153
|
+
|
|
154
|
+
format = InputSchema.detect_format(schema)
|
|
155
|
+
InputSchema.new(schema: schema, format: format)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Extract framework extensions from frontmatter
|
|
159
|
+
#
|
|
160
|
+
# @param frontmatter [Hash] Full frontmatter hash
|
|
161
|
+
# @return [Hash] Framework extensions
|
|
162
|
+
def extract_extensions(frontmatter)
|
|
163
|
+
extensions = {}
|
|
164
|
+
|
|
165
|
+
%w[activeagent crewai langchain genkit dotprompt].each do |framework|
|
|
166
|
+
value = frontmatter[framework] || frontmatter[framework.to_sym]
|
|
167
|
+
extensions[framework.to_sym] = value if value.present?
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
extensions
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Extract instructions from markdown body
|
|
174
|
+
#
|
|
175
|
+
# @param body [String] Markdown body
|
|
176
|
+
# @return [String, nil] Extracted instructions
|
|
177
|
+
def extract_instructions(body)
|
|
178
|
+
return nil if body.blank?
|
|
179
|
+
|
|
180
|
+
# Try to find ## Instructions section
|
|
181
|
+
if body =~ /##\s*Instructions\s*\n(.*?)(?=\n##|\z)/mi
|
|
182
|
+
return ::Regexp.last_match(1).strip
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Try ## Instruct (abbreviated)
|
|
186
|
+
if body =~ /##\s*Instruct\w*\s*\n(.*?)(?=\n##|\z)/mi
|
|
187
|
+
return ::Regexp.last_match(1).strip
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Fall back to body without title
|
|
191
|
+
body.gsub(/\A#\s+[^\n]+\n+/, "").strip.presence
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Parse config/model parameters
|
|
195
|
+
#
|
|
196
|
+
# @param frontmatter [Hash] Frontmatter hash
|
|
197
|
+
# @return [Hash] Config hash
|
|
198
|
+
def parse_config(frontmatter)
|
|
199
|
+
config = frontmatter["config"] || frontmatter[:config] || {}
|
|
200
|
+
|
|
201
|
+
# Also check for top-level config keys (Dotprompt style)
|
|
202
|
+
%w[temperature max_tokens maxOutputTokens top_p topP top_k topK stop stopSequences].each do |key|
|
|
203
|
+
value = frontmatter[key] || frontmatter[key.to_sym]
|
|
204
|
+
if value.present?
|
|
205
|
+
# Normalize key names
|
|
206
|
+
normalized_key = case key
|
|
207
|
+
when "maxOutputTokens" then "max_tokens"
|
|
208
|
+
when "topP" then "top_p"
|
|
209
|
+
when "topK" then "top_k"
|
|
210
|
+
when "stopSequences" then "stop"
|
|
211
|
+
else key
|
|
212
|
+
end
|
|
213
|
+
config[normalized_key] = value
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
config.presence || {}
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidAgent
|
|
4
|
+
module AgentManifest
|
|
5
|
+
module Parsers
|
|
6
|
+
# CrewAIParser handles CrewAI's agents.yaml format.
|
|
7
|
+
#
|
|
8
|
+
# CrewAI defines agents with role, goal, and backstory.
|
|
9
|
+
# This parser converts them to the unified Manifest format.
|
|
10
|
+
#
|
|
11
|
+
# @see https://docs.crewai.com
|
|
12
|
+
#
|
|
13
|
+
# @example agents.yaml
|
|
14
|
+
# research_analyst:
|
|
15
|
+
# role: >
|
|
16
|
+
# Senior Research Analyst
|
|
17
|
+
# goal: >
|
|
18
|
+
# Uncover cutting-edge developments
|
|
19
|
+
# backstory: >
|
|
20
|
+
# You're a seasoned researcher...
|
|
21
|
+
# llm: claude-sonnet-4-20250514
|
|
22
|
+
# tools:
|
|
23
|
+
# - SerperDevTool
|
|
24
|
+
#
|
|
25
|
+
class CrewAIParser < BaseParser
|
|
26
|
+
class << self
|
|
27
|
+
def format_name
|
|
28
|
+
:crewai
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def parse_string(content)
|
|
32
|
+
data = YAML.safe_load(content, permitted_classes: [Symbol])
|
|
33
|
+
|
|
34
|
+
raise ParseError, "Invalid CrewAI YAML: expected hash" unless data.is_a?(Hash)
|
|
35
|
+
|
|
36
|
+
# CrewAI files can contain multiple agents
|
|
37
|
+
agents = data.map do |agent_name, agent_data|
|
|
38
|
+
parse_agent(agent_name.to_s, agent_data)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# If single agent, return it directly; otherwise return array
|
|
42
|
+
agents.size == 1 ? agents.first : agents
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Parse a file containing multiple agents
|
|
46
|
+
#
|
|
47
|
+
# @param agents_path [String] Path to agents.yaml
|
|
48
|
+
# @param tasks_path [String, nil] Optional path to tasks.yaml
|
|
49
|
+
# @return [Array<Manifest>] Parsed manifests
|
|
50
|
+
def parse_with_tasks(agents_path, tasks_path = nil)
|
|
51
|
+
agents_content = File.read(agents_path, encoding: "UTF-8")
|
|
52
|
+
result = parse_string(agents_content)
|
|
53
|
+
|
|
54
|
+
# Ensure we have an array
|
|
55
|
+
manifests = result.is_a?(Array) ? result : [result]
|
|
56
|
+
|
|
57
|
+
# Optionally merge task information
|
|
58
|
+
if tasks_path && File.exist?(tasks_path)
|
|
59
|
+
tasks = YAML.safe_load(File.read(tasks_path, encoding: "UTF-8"))
|
|
60
|
+
merge_tasks(manifests, tasks)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
manifests
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def parse_agent(name, data)
|
|
69
|
+
return nil unless data.is_a?(Hash)
|
|
70
|
+
|
|
71
|
+
Manifest.new(
|
|
72
|
+
# Convert snake_case name to kebab-case
|
|
73
|
+
name: name.tr("_", "-"),
|
|
74
|
+
description: clean_multiline(data["goal"]),
|
|
75
|
+
|
|
76
|
+
# Model
|
|
77
|
+
model: normalize_model(data["llm"]),
|
|
78
|
+
config: parse_crewai_config(data),
|
|
79
|
+
|
|
80
|
+
# Build instructions from role + backstory
|
|
81
|
+
instructions: build_instructions(data),
|
|
82
|
+
template: build_template(data),
|
|
83
|
+
|
|
84
|
+
# Tools (as references to Python classes)
|
|
85
|
+
tools: parse_crewai_tools(data["tools"]),
|
|
86
|
+
|
|
87
|
+
# Preserve CrewAI-specific config
|
|
88
|
+
extensions: {
|
|
89
|
+
crewai: build_crewai_extensions(data)
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Parse CrewAI config options
|
|
95
|
+
def parse_crewai_config(data)
|
|
96
|
+
config = {}
|
|
97
|
+
|
|
98
|
+
# CrewAI doesn't have direct model config in YAML typically
|
|
99
|
+
# but some implementations support it
|
|
100
|
+
config[:temperature] = data["temperature"] if data["temperature"]
|
|
101
|
+
config[:max_tokens] = data["max_tokens"] if data["max_tokens"]
|
|
102
|
+
|
|
103
|
+
config.compact
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Build instructions from role and backstory
|
|
107
|
+
def build_instructions(data)
|
|
108
|
+
parts = []
|
|
109
|
+
|
|
110
|
+
role = clean_multiline(data["role"])
|
|
111
|
+
goal = clean_multiline(data["goal"])
|
|
112
|
+
backstory = clean_multiline(data["backstory"])
|
|
113
|
+
|
|
114
|
+
parts << "Role: #{role}" if role.present?
|
|
115
|
+
parts << "Goal: #{goal}" if goal.present?
|
|
116
|
+
parts << "\n#{backstory}" if backstory.present?
|
|
117
|
+
|
|
118
|
+
parts.join("\n").strip
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Build a markdown template from CrewAI data
|
|
122
|
+
def build_template(data)
|
|
123
|
+
role = clean_multiline(data["role"]) || "Agent"
|
|
124
|
+
goal = clean_multiline(data["goal"])
|
|
125
|
+
backstory = clean_multiline(data["backstory"])
|
|
126
|
+
|
|
127
|
+
template = "# #{role}\n\n"
|
|
128
|
+
template += "## Goal\n\n#{goal}\n\n" if goal
|
|
129
|
+
template += "## Backstory\n\n#{backstory}\n\n" if backstory
|
|
130
|
+
template
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Parse CrewAI tool references
|
|
134
|
+
def parse_crewai_tools(tools_data)
|
|
135
|
+
return [] unless tools_data.is_a?(Array)
|
|
136
|
+
|
|
137
|
+
tools_data.map do |tool|
|
|
138
|
+
if tool.is_a?(String)
|
|
139
|
+
# Reference to a Python tool class
|
|
140
|
+
Tool.new(ref: "crewai://#{tool}")
|
|
141
|
+
elsif tool.is_a?(Hash)
|
|
142
|
+
Tool.from_hash(tool)
|
|
143
|
+
else
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
146
|
+
end.compact
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Build CrewAI-specific extensions
|
|
150
|
+
def build_crewai_extensions(data)
|
|
151
|
+
{
|
|
152
|
+
role: clean_multiline(data["role"]),
|
|
153
|
+
goal: clean_multiline(data["goal"]),
|
|
154
|
+
backstory: clean_multiline(data["backstory"]),
|
|
155
|
+
verbose: data["verbose"],
|
|
156
|
+
allow_delegation: data["allow_delegation"],
|
|
157
|
+
max_iter: data["max_iter"],
|
|
158
|
+
max_rpm: data["max_rpm"],
|
|
159
|
+
memory: data["memory"],
|
|
160
|
+
cache: data["cache"],
|
|
161
|
+
step_callback: data["step_callback"],
|
|
162
|
+
system_template: data["system_template"],
|
|
163
|
+
prompt_template: data["prompt_template"],
|
|
164
|
+
response_template: data["response_template"]
|
|
165
|
+
}.compact
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Clean up YAML multiline strings
|
|
169
|
+
def clean_multiline(value)
|
|
170
|
+
return nil unless value
|
|
171
|
+
value.to_s.strip.gsub(/\s+/, " ")
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Merge task information into manifests
|
|
175
|
+
def merge_tasks(manifests, tasks)
|
|
176
|
+
# Tasks in CrewAI are associated with agents
|
|
177
|
+
# This is a simplified merge - full implementation would be more complex
|
|
178
|
+
tasks.each do |task_name, task_data|
|
|
179
|
+
agent_name = task_data["agent"]&.tr("_", "-")
|
|
180
|
+
manifest = manifests.find { |m| m.name == agent_name }
|
|
181
|
+
|
|
182
|
+
if manifest
|
|
183
|
+
# Add task information to extensions
|
|
184
|
+
manifest.extensions[:crewai] ||= {}
|
|
185
|
+
manifest.extensions[:crewai][:tasks] ||= []
|
|
186
|
+
manifest.extensions[:crewai][:tasks] << {
|
|
187
|
+
name: task_name,
|
|
188
|
+
description: task_data["description"],
|
|
189
|
+
expected_output: task_data["expected_output"]
|
|
190
|
+
}
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Register the parser
|
|
198
|
+
ParserRegistry.register(:crewai, CrewAIParser)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidAgent
|
|
4
|
+
module AgentManifest
|
|
5
|
+
module Parsers
|
|
6
|
+
# DotpromptParser handles Google Genkit's .prompt format.
|
|
7
|
+
#
|
|
8
|
+
# Dotprompt files use YAML frontmatter with Handlebars-style templates.
|
|
9
|
+
# This parser converts them to the unified Manifest format.
|
|
10
|
+
#
|
|
11
|
+
# @see https://github.com/google/dotprompt
|
|
12
|
+
#
|
|
13
|
+
# @example Basic .prompt file
|
|
14
|
+
# ---
|
|
15
|
+
# model: googleai/gemini-1.5-pro
|
|
16
|
+
# input:
|
|
17
|
+
# schema:
|
|
18
|
+
# text: string
|
|
19
|
+
# output:
|
|
20
|
+
# format: json
|
|
21
|
+
# ---
|
|
22
|
+
#
|
|
23
|
+
# Extract information from: {{text}}
|
|
24
|
+
#
|
|
25
|
+
class DotpromptParser < BaseParser
|
|
26
|
+
class << self
|
|
27
|
+
def format_name
|
|
28
|
+
:dotprompt
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def parse_string(content)
|
|
32
|
+
frontmatter, body = extract_frontmatter(content)
|
|
33
|
+
|
|
34
|
+
# Extract name from frontmatter or generate from content
|
|
35
|
+
name = frontmatter["name"] || generate_name_from_content(body)
|
|
36
|
+
|
|
37
|
+
Manifest.new(
|
|
38
|
+
name: name,
|
|
39
|
+
version: frontmatter["version"] || "1.0.0",
|
|
40
|
+
description: frontmatter["description"],
|
|
41
|
+
|
|
42
|
+
# Model - Dotprompt format
|
|
43
|
+
model: normalize_model(frontmatter["model"]),
|
|
44
|
+
config: parse_dotprompt_config(frontmatter),
|
|
45
|
+
|
|
46
|
+
# Schemas
|
|
47
|
+
input_schema: parse_input_schema(frontmatter["input"]),
|
|
48
|
+
output_schema: parse_output_schema(frontmatter["output"]),
|
|
49
|
+
|
|
50
|
+
# Tools (Dotprompt can reference tools)
|
|
51
|
+
tools: parse_tools(frontmatter["tools"]),
|
|
52
|
+
|
|
53
|
+
# Template
|
|
54
|
+
instructions: body,
|
|
55
|
+
template: body,
|
|
56
|
+
|
|
57
|
+
# Store original Dotprompt config in extensions
|
|
58
|
+
extensions: {
|
|
59
|
+
dotprompt: extract_dotprompt_extensions(frontmatter)
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# Generate a name from template content
|
|
67
|
+
def generate_name_from_content(body)
|
|
68
|
+
# Try to extract from first line/heading
|
|
69
|
+
if body =~ /\A#\s+(.+)/
|
|
70
|
+
::Regexp.last_match(1).strip.downcase.gsub(/\s+/, "-").gsub(/[^a-z0-9-]/, "")
|
|
71
|
+
else
|
|
72
|
+
"unnamed-prompt"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Parse Dotprompt-specific config fields
|
|
77
|
+
def parse_dotprompt_config(frontmatter)
|
|
78
|
+
config = {}
|
|
79
|
+
|
|
80
|
+
# Map Dotprompt field names to normalized names
|
|
81
|
+
config[:temperature] = frontmatter["temperature"] if frontmatter["temperature"]
|
|
82
|
+
config[:max_tokens] = frontmatter["maxOutputTokens"] if frontmatter["maxOutputTokens"]
|
|
83
|
+
config[:top_p] = frontmatter["topP"] if frontmatter["topP"]
|
|
84
|
+
config[:top_k] = frontmatter["topK"] if frontmatter["topK"]
|
|
85
|
+
config[:stop] = frontmatter["stopSequences"] if frontmatter["stopSequences"]
|
|
86
|
+
|
|
87
|
+
# Also support normalized names
|
|
88
|
+
config[:temperature] ||= frontmatter["config"]&.dig("temperature")
|
|
89
|
+
config[:max_tokens] ||= frontmatter["config"]&.dig("max_tokens")
|
|
90
|
+
|
|
91
|
+
config.compact
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Parse Dotprompt output schema
|
|
95
|
+
def parse_output_schema(output_data)
|
|
96
|
+
return nil unless output_data.is_a?(Hash)
|
|
97
|
+
|
|
98
|
+
{
|
|
99
|
+
format: output_data["format"],
|
|
100
|
+
schema: output_data["schema"]
|
|
101
|
+
}.compact.presence
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Extract Dotprompt-specific extensions that don't map to standard fields
|
|
105
|
+
def extract_dotprompt_extensions(frontmatter)
|
|
106
|
+
extensions = {}
|
|
107
|
+
|
|
108
|
+
# Preserve Dotprompt-specific fields
|
|
109
|
+
%w[candidates cache default variant metadata].each do |key|
|
|
110
|
+
extensions[key] = frontmatter[key] if frontmatter[key].present?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
extensions.presence
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Register the parser
|
|
119
|
+
ParserRegistry.register(:dotprompt, DotpromptParser)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidAgent
|
|
4
|
+
module AgentManifest
|
|
5
|
+
module Parsers
|
|
6
|
+
# GitHubPromptParser handles GitHub Copilot's .prompt.md format.
|
|
7
|
+
#
|
|
8
|
+
# GitHub Copilot prompt files use Markdown with YAML frontmatter,
|
|
9
|
+
# supporting variables, tool references, and agent configuration.
|
|
10
|
+
#
|
|
11
|
+
# @see https://docs.github.com/en/copilot/tutorials/customization-library/prompt-files
|
|
12
|
+
#
|
|
13
|
+
# @example .prompt.md file
|
|
14
|
+
# ---
|
|
15
|
+
# model: GPT-4o
|
|
16
|
+
# tools: ['githubRepo', 'search/codebase']
|
|
17
|
+
# description: 'Generate a new React form component'
|
|
18
|
+
# ---
|
|
19
|
+
#
|
|
20
|
+
# Create a React form component for ${input:formName}
|
|
21
|
+
#
|
|
22
|
+
class GitHubPromptParser < BaseParser
|
|
23
|
+
class << self
|
|
24
|
+
def format_name
|
|
25
|
+
:github_prompt
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def parse_string(content)
|
|
29
|
+
frontmatter, body = extract_frontmatter(content)
|
|
30
|
+
|
|
31
|
+
Manifest.new(
|
|
32
|
+
name: extract_name(frontmatter, body),
|
|
33
|
+
description: frontmatter["description"],
|
|
34
|
+
version: "1.0.0",
|
|
35
|
+
|
|
36
|
+
# Model
|
|
37
|
+
model: normalize_github_model(frontmatter["model"]),
|
|
38
|
+
config: {},
|
|
39
|
+
|
|
40
|
+
# Tools are GitHub-specific references
|
|
41
|
+
tools: parse_github_tools(frontmatter["tools"]),
|
|
42
|
+
|
|
43
|
+
# Template with GitHub variable syntax
|
|
44
|
+
instructions: body,
|
|
45
|
+
template: body,
|
|
46
|
+
|
|
47
|
+
# Preserve GitHub-specific config
|
|
48
|
+
extensions: {
|
|
49
|
+
github_prompt: extract_github_extensions(frontmatter)
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Extract name from frontmatter or generate from content
|
|
57
|
+
def extract_name(frontmatter, body)
|
|
58
|
+
return frontmatter["name"] if frontmatter["name"].present?
|
|
59
|
+
|
|
60
|
+
# Try to extract from description
|
|
61
|
+
if frontmatter["description"].present?
|
|
62
|
+
return frontmatter["description"]
|
|
63
|
+
.downcase
|
|
64
|
+
.gsub(/[^a-z0-9\s-]/, "")
|
|
65
|
+
.gsub(/\s+/, "-")
|
|
66
|
+
.slice(0, 50)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Try to extract from first heading
|
|
70
|
+
if body =~ /\A#\s+(.+)/
|
|
71
|
+
return ::Regexp.last_match(1)
|
|
72
|
+
.strip
|
|
73
|
+
.downcase
|
|
74
|
+
.gsub(/[^a-z0-9\s-]/, "")
|
|
75
|
+
.gsub(/\s+/, "-")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
"github-prompt"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Normalize GitHub model names
|
|
82
|
+
def normalize_github_model(model)
|
|
83
|
+
return nil unless model.present?
|
|
84
|
+
|
|
85
|
+
model_str = model.to_s.strip
|
|
86
|
+
|
|
87
|
+
# GitHub uses simplified model names
|
|
88
|
+
case model_str.downcase
|
|
89
|
+
when "gpt-4o", "gpt4o"
|
|
90
|
+
"openai/gpt-4o"
|
|
91
|
+
when "gpt-4", "gpt4"
|
|
92
|
+
"openai/gpt-4"
|
|
93
|
+
when "gpt-3.5-turbo", "gpt35turbo"
|
|
94
|
+
"openai/gpt-3.5-turbo"
|
|
95
|
+
when /\Aclaude/
|
|
96
|
+
"anthropic/#{model_str.downcase}"
|
|
97
|
+
else
|
|
98
|
+
normalize_model(model_str)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Parse GitHub tool references
|
|
103
|
+
def parse_github_tools(tools_data)
|
|
104
|
+
return [] unless tools_data.is_a?(Array)
|
|
105
|
+
|
|
106
|
+
tools_data.map do |tool|
|
|
107
|
+
tool_name = tool.to_s
|
|
108
|
+
Tool.new(ref: "github://#{tool_name}")
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Extract GitHub-specific extensions
|
|
113
|
+
def extract_github_extensions(frontmatter)
|
|
114
|
+
{
|
|
115
|
+
agent: frontmatter["agent"],
|
|
116
|
+
mode: frontmatter["mode"],
|
|
117
|
+
variables: extract_variables(frontmatter),
|
|
118
|
+
# Preserve any other GitHub-specific fields
|
|
119
|
+
original_tools: frontmatter["tools"]
|
|
120
|
+
}.compact
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Extract variable definitions
|
|
124
|
+
def extract_variables(frontmatter)
|
|
125
|
+
variables = {}
|
|
126
|
+
|
|
127
|
+
# GitHub prompts can define input variables
|
|
128
|
+
frontmatter.each do |key, value|
|
|
129
|
+
next unless key.to_s.start_with?("input:")
|
|
130
|
+
var_name = key.to_s.sub("input:", "")
|
|
131
|
+
variables[var_name] = value
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
variables.presence
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Register the parser
|
|
140
|
+
ParserRegistry.register(:github_prompt, GitHubPromptParser)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|