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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +68 -0
- data/LICENSE +21 -0
- data/README.md +209 -18
- data/Rakefile +22 -2
- 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 +2 -2
- data/lib/generators/solid_agent/agent/templates/agent.rb.erb +3 -3
- data/lib/generators/solid_agent/context/templates/context_model.rb.erb +50 -16
- data/lib/generators/solid_agent/context/templates/create_generations.rb.erb +8 -0
- data/lib/generators/solid_agent/context/templates/create_messages.rb.erb +4 -0
- data/lib/generators/solid_agent/context/templates/generation_model.rb.erb +11 -0
- data/lib/generators/solid_agent/install/install_generator.rb +9 -0
- data/lib/generators/solid_agent/install/templates/agent_context.rb.erb +60 -17
- data/lib/generators/solid_agent/install/templates/agent_generation.rb.erb +23 -6
- 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_run.rb.erb +122 -0
- data/lib/generators/solid_agent/install/templates/create_agent_generations.rb.erb +13 -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 +5 -0
- data/lib/generators/solid_agent/install/templates/create_agent_runs.rb.erb +46 -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/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/has_context.rb +251 -30
- data/lib/solid_agent/has_memory.rb +136 -0
- data/lib/solid_agent/has_reasons.rb +230 -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/tool_cache.rb +91 -0
- data/lib/solid_agent/version.rb +1 -1
- data/lib/solid_agent.rb +70 -3
- metadata +87 -1
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
|
|
8
|
+
module SolidAgent
|
|
9
|
+
module AgentManifest
|
|
10
|
+
module Registry
|
|
11
|
+
# Client provides HTTP API access to the ActiveAgents registry.
|
|
12
|
+
#
|
|
13
|
+
# @example Search for agents
|
|
14
|
+
# client = Registry::Client.new
|
|
15
|
+
# results = client.search(q: "research", tags: ["assistant"])
|
|
16
|
+
#
|
|
17
|
+
# @example Download an agent
|
|
18
|
+
# client.download("@anthropic/research-assistant", path: "./agents/")
|
|
19
|
+
#
|
|
20
|
+
# @example Publish an agent
|
|
21
|
+
# client = Registry::Client.new(token: "aa_live_xxx")
|
|
22
|
+
# client.publish("./research-assistant.agent.md")
|
|
23
|
+
#
|
|
24
|
+
class Client
|
|
25
|
+
BASE_URL = "https://api.activeagents.ai/v1"
|
|
26
|
+
|
|
27
|
+
attr_reader :base_url, :token
|
|
28
|
+
|
|
29
|
+
# Initialize a new client
|
|
30
|
+
#
|
|
31
|
+
# @param token [String, nil] Auth token (uses Auth.token if nil)
|
|
32
|
+
# @param base_url [String] API base URL
|
|
33
|
+
def initialize(token: nil, base_url: BASE_URL)
|
|
34
|
+
@token = token || Auth.token
|
|
35
|
+
@base_url = base_url
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# === Discovery Methods ===
|
|
39
|
+
|
|
40
|
+
# Search for agents
|
|
41
|
+
#
|
|
42
|
+
# @param q [String, nil] Search query
|
|
43
|
+
# @param tags [Array<String>, nil] Filter by tags
|
|
44
|
+
# @param framework [String, nil] Filter by framework (e.g., "activeagent", "crewai")
|
|
45
|
+
# @param author [String, nil] Filter by author
|
|
46
|
+
# @param sort [String] Sort order ("popular", "recent", "name")
|
|
47
|
+
# @param page [Integer] Page number
|
|
48
|
+
# @param per_page [Integer] Results per page
|
|
49
|
+
# @return [Hash] Search results with :agents, :total, :page keys
|
|
50
|
+
def search(q: nil, tags: nil, framework: nil, author: nil, sort: "popular", page: 1, per_page: 20)
|
|
51
|
+
params = {
|
|
52
|
+
q: q,
|
|
53
|
+
tags: tags&.join(","),
|
|
54
|
+
framework: framework,
|
|
55
|
+
author: author,
|
|
56
|
+
sort: sort,
|
|
57
|
+
page: page,
|
|
58
|
+
per_page: per_page
|
|
59
|
+
}.compact
|
|
60
|
+
|
|
61
|
+
get("/agents", params)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get an agent by name
|
|
65
|
+
#
|
|
66
|
+
# @param name [String] Agent name (e.g., "@anthropic/research-assistant")
|
|
67
|
+
# @param version [String, nil] Specific version (latest if nil)
|
|
68
|
+
# @return [Hash] Agent metadata
|
|
69
|
+
def get(name, version: nil)
|
|
70
|
+
path = version ? "/agents/#{encode_name(name)}/versions/#{version}" : "/agents/#{encode_name(name)}"
|
|
71
|
+
get_request(path)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# List all versions of an agent
|
|
75
|
+
#
|
|
76
|
+
# @param name [String] Agent name
|
|
77
|
+
# @return [Array<Hash>] Version list
|
|
78
|
+
def versions(name)
|
|
79
|
+
get_request("/agents/#{encode_name(name)}/versions")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# === Download Methods ===
|
|
83
|
+
|
|
84
|
+
# Download an agent manifest
|
|
85
|
+
#
|
|
86
|
+
# @param name [String] Agent name
|
|
87
|
+
# @param version [String, nil] Specific version
|
|
88
|
+
# @param path [String, nil] Download directory (current dir if nil)
|
|
89
|
+
# @param format [Symbol] Output format (:agent_md, :dotprompt, etc.)
|
|
90
|
+
# @return [String] Path to downloaded file
|
|
91
|
+
def download(name, version: nil, path: nil, format: :agent_md)
|
|
92
|
+
# Get agent metadata
|
|
93
|
+
agent = get(name, version: version)
|
|
94
|
+
|
|
95
|
+
# Determine output path
|
|
96
|
+
output_dir = path || Dir.pwd
|
|
97
|
+
FileUtils.mkdir_p(output_dir)
|
|
98
|
+
|
|
99
|
+
filename = agent_filename(agent, format)
|
|
100
|
+
output_path = File.join(output_dir, filename)
|
|
101
|
+
|
|
102
|
+
# Download content
|
|
103
|
+
content_url = agent["download_url"] || agent.dig("links", "download")
|
|
104
|
+
if content_url
|
|
105
|
+
content = fetch_content(content_url)
|
|
106
|
+
else
|
|
107
|
+
# Fetch from content endpoint
|
|
108
|
+
content = get_request("/agents/#{encode_name(name)}/content", accept: "text/markdown")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Convert format if needed
|
|
112
|
+
source_format = agent["format"]&.to_sym || :agent_md
|
|
113
|
+
if format != source_format
|
|
114
|
+
manifest = ParserRegistry.parse_string(content, format: source_format)
|
|
115
|
+
content = ExporterRegistry.export(manifest, format)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
File.write(output_path, content, encoding: "UTF-8")
|
|
119
|
+
output_path
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Download a specific file from an agent package
|
|
123
|
+
#
|
|
124
|
+
# @param name [String] Agent name
|
|
125
|
+
# @param file_path [String] Path within the package
|
|
126
|
+
# @param version [String, nil] Specific version
|
|
127
|
+
# @return [String] File content
|
|
128
|
+
def download_file(name, file_path, version: nil)
|
|
129
|
+
path = "/agents/#{encode_name(name)}/files/#{file_path}"
|
|
130
|
+
path += "?version=#{version}" if version
|
|
131
|
+
get_request(path, accept: "*/*")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# === Publishing Methods ===
|
|
135
|
+
|
|
136
|
+
# Publish an agent manifest
|
|
137
|
+
#
|
|
138
|
+
# @param path [String] Path to manifest file
|
|
139
|
+
# @param tag [String] Version tag ("latest", "beta", etc.)
|
|
140
|
+
# @param scope [String, nil] Organization scope
|
|
141
|
+
# @param access [String] Access level ("public", "private")
|
|
142
|
+
# @return [Hash] Published agent info
|
|
143
|
+
def publish(path, tag: "latest", scope: nil, access: "public")
|
|
144
|
+
require_auth!
|
|
145
|
+
|
|
146
|
+
manifest = AgentManifest.parse(path)
|
|
147
|
+
AgentManifest.validate!(manifest, strict: true)
|
|
148
|
+
|
|
149
|
+
content = File.read(path, encoding: "UTF-8")
|
|
150
|
+
format = ParserRegistry.detect_format(path)
|
|
151
|
+
|
|
152
|
+
body = {
|
|
153
|
+
name: scope ? "#{scope}/#{manifest.name}" : manifest.name,
|
|
154
|
+
version: manifest.version,
|
|
155
|
+
tag: tag,
|
|
156
|
+
access: access,
|
|
157
|
+
format: format,
|
|
158
|
+
content: content,
|
|
159
|
+
metadata: {
|
|
160
|
+
description: manifest.description,
|
|
161
|
+
tags: manifest.tags,
|
|
162
|
+
model: manifest.model,
|
|
163
|
+
author: manifest.author,
|
|
164
|
+
license: manifest.license,
|
|
165
|
+
repository: manifest.repository
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
post("/agents", body)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Deprecate a version
|
|
173
|
+
#
|
|
174
|
+
# @param name [String] Agent name
|
|
175
|
+
# @param version [String] Version to deprecate
|
|
176
|
+
# @param message [String] Deprecation message
|
|
177
|
+
# @return [Hash] Updated version info
|
|
178
|
+
def deprecate(name, version, message:)
|
|
179
|
+
require_auth!
|
|
180
|
+
patch("/agents/#{encode_name(name)}/versions/#{version}", { deprecated: true, deprecation_message: message })
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Unpublish a version
|
|
184
|
+
#
|
|
185
|
+
# @param name [String] Agent name
|
|
186
|
+
# @param version [String] Version to remove
|
|
187
|
+
# @return [Boolean]
|
|
188
|
+
def unpublish(name, version)
|
|
189
|
+
require_auth!
|
|
190
|
+
delete("/agents/#{encode_name(name)}/versions/#{version}")
|
|
191
|
+
true
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# === User Actions ===
|
|
195
|
+
|
|
196
|
+
# Star an agent
|
|
197
|
+
#
|
|
198
|
+
# @param name [String] Agent name
|
|
199
|
+
# @return [Boolean]
|
|
200
|
+
def star(name)
|
|
201
|
+
require_auth!
|
|
202
|
+
post("/agents/#{encode_name(name)}/star", {})
|
|
203
|
+
true
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Unstar an agent
|
|
207
|
+
#
|
|
208
|
+
# @param name [String] Agent name
|
|
209
|
+
# @return [Boolean]
|
|
210
|
+
def unstar(name)
|
|
211
|
+
require_auth!
|
|
212
|
+
delete("/agents/#{encode_name(name)}/star")
|
|
213
|
+
true
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Fork an agent
|
|
217
|
+
#
|
|
218
|
+
# @param name [String] Source agent name
|
|
219
|
+
# @param new_name [String] Name for the fork
|
|
220
|
+
# @param scope [String, nil] Organization scope
|
|
221
|
+
# @return [Hash] Forked agent info
|
|
222
|
+
def fork(name, new_name:, scope: nil)
|
|
223
|
+
require_auth!
|
|
224
|
+
post("/agents/#{encode_name(name)}/fork", { new_name: new_name, scope: scope })
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# === Sandbox Methods ===
|
|
228
|
+
|
|
229
|
+
# Run an agent in the sandbox
|
|
230
|
+
#
|
|
231
|
+
# @param name [String] Agent name
|
|
232
|
+
# @param input [Hash] Input parameters
|
|
233
|
+
# @param model [String, nil] Override model
|
|
234
|
+
# @param stream [Boolean] Enable streaming
|
|
235
|
+
# @return [Hash] Run result or job info
|
|
236
|
+
def run(name, input:, model: nil, stream: false)
|
|
237
|
+
require_auth!
|
|
238
|
+
body = { input: input, model: model, stream: stream }.compact
|
|
239
|
+
post("/agents/#{encode_name(name)}/run", body)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Get status of a sandbox run
|
|
243
|
+
#
|
|
244
|
+
# @param run_id [String] Run ID
|
|
245
|
+
# @return [Hash] Run status
|
|
246
|
+
def run_status(run_id)
|
|
247
|
+
require_auth!
|
|
248
|
+
get_request("/runs/#{run_id}")
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# === Account Methods ===
|
|
252
|
+
|
|
253
|
+
# Get current user info
|
|
254
|
+
#
|
|
255
|
+
# @return [Hash] User info
|
|
256
|
+
def me
|
|
257
|
+
require_auth!
|
|
258
|
+
get_request("/me")
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Get user's starred agents
|
|
262
|
+
#
|
|
263
|
+
# @return [Array<Hash>] Starred agents
|
|
264
|
+
def my_stars
|
|
265
|
+
require_auth!
|
|
266
|
+
get_request("/me/stars")
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Get user's published agents
|
|
270
|
+
#
|
|
271
|
+
# @return [Array<Hash>] Published agents
|
|
272
|
+
def my_agents
|
|
273
|
+
require_auth!
|
|
274
|
+
get_request("/me/agents")
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
private
|
|
278
|
+
|
|
279
|
+
def require_auth!
|
|
280
|
+
raise RegistryError, "Authentication required" unless token
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def encode_name(name)
|
|
284
|
+
# Handle scoped names: @scope/name -> @scope%2Fname
|
|
285
|
+
URI.encode_www_form_component(name)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def agent_filename(agent, format)
|
|
289
|
+
name = agent["name"].to_s.split("/").last
|
|
290
|
+
extension = case format
|
|
291
|
+
when :agent_md then ".agent.md"
|
|
292
|
+
when :dotprompt then ".prompt"
|
|
293
|
+
when :crewai then ".yaml"
|
|
294
|
+
when :github_prompt then ".prompt.md"
|
|
295
|
+
else ".agent.md"
|
|
296
|
+
end
|
|
297
|
+
"#{name}#{extension}"
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def get_request(path, params = {}, accept: "application/json")
|
|
301
|
+
uri = build_uri(path, params.except(:accept))
|
|
302
|
+
request = Net::HTTP::Get.new(uri)
|
|
303
|
+
request["Accept"] = accept
|
|
304
|
+
execute(request)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def post(path, body)
|
|
308
|
+
uri = build_uri(path)
|
|
309
|
+
request = Net::HTTP::Post.new(uri)
|
|
310
|
+
request["Content-Type"] = "application/json"
|
|
311
|
+
request.body = body.to_json
|
|
312
|
+
execute(request)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def patch(path, body)
|
|
316
|
+
uri = build_uri(path)
|
|
317
|
+
request = Net::HTTP::Patch.new(uri)
|
|
318
|
+
request["Content-Type"] = "application/json"
|
|
319
|
+
request.body = body.to_json
|
|
320
|
+
execute(request)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def delete(path)
|
|
324
|
+
uri = build_uri(path)
|
|
325
|
+
request = Net::HTTP::Delete.new(uri)
|
|
326
|
+
execute(request)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def build_uri(path, params = {})
|
|
330
|
+
uri = URI.parse("#{base_url}#{path}")
|
|
331
|
+
uri.query = URI.encode_www_form(params) if params.any?
|
|
332
|
+
uri
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def execute(request)
|
|
336
|
+
# Add auth header if available
|
|
337
|
+
if token
|
|
338
|
+
request["Authorization"] = "Bearer #{token}"
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
request["User-Agent"] = "SolidAgent/#{SolidAgent::VERSION}"
|
|
342
|
+
|
|
343
|
+
uri = request.uri
|
|
344
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
345
|
+
http.use_ssl = uri.scheme == "https"
|
|
346
|
+
http.open_timeout = 10
|
|
347
|
+
http.read_timeout = 30
|
|
348
|
+
|
|
349
|
+
response = http.request(request)
|
|
350
|
+
|
|
351
|
+
case response
|
|
352
|
+
when Net::HTTPSuccess
|
|
353
|
+
parse_response(response)
|
|
354
|
+
when Net::HTTPUnauthorized
|
|
355
|
+
raise RegistryError, "Authentication failed. Please login again."
|
|
356
|
+
when Net::HTTPForbidden
|
|
357
|
+
raise RegistryError, "Access denied"
|
|
358
|
+
when Net::HTTPNotFound
|
|
359
|
+
raise RegistryError, "Resource not found"
|
|
360
|
+
when Net::HTTPUnprocessableEntity
|
|
361
|
+
error_body = parse_response(response) rescue {}
|
|
362
|
+
message = error_body["error"] || error_body["message"] || "Validation failed"
|
|
363
|
+
raise ValidationError, message
|
|
364
|
+
else
|
|
365
|
+
error_body = parse_response(response) rescue {}
|
|
366
|
+
message = error_body["error"] || error_body["message"] || "Request failed: #{response.code}"
|
|
367
|
+
raise RegistryError, message
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def parse_response(response)
|
|
372
|
+
return response.body unless response["Content-Type"]&.include?("application/json")
|
|
373
|
+
|
|
374
|
+
JSON.parse(response.body)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def fetch_content(url)
|
|
378
|
+
uri = URI.parse(url)
|
|
379
|
+
Net::HTTP.get(uri)
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidAgent
|
|
4
|
+
module AgentManifest
|
|
5
|
+
# Resource represents an external data source that an agent can access.
|
|
6
|
+
#
|
|
7
|
+
# Resources follow MCP (Model Context Protocol) conventions and can represent
|
|
8
|
+
# files, APIs, databases, or other data sources.
|
|
9
|
+
#
|
|
10
|
+
# @example File resource
|
|
11
|
+
# resource = Resource.new(
|
|
12
|
+
# name: "company_docs",
|
|
13
|
+
# description: "Internal company documentation",
|
|
14
|
+
# uri: "file:///docs/**/*.md",
|
|
15
|
+
# mime_type: "text/markdown"
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
# @example API resource
|
|
19
|
+
# resource = Resource.new(
|
|
20
|
+
# name: "api_spec",
|
|
21
|
+
# description: "OpenAPI specification",
|
|
22
|
+
# uri: "https://api.example.com/openapi.json",
|
|
23
|
+
# mime_type: "application/json"
|
|
24
|
+
# )
|
|
25
|
+
#
|
|
26
|
+
class Resource
|
|
27
|
+
# @return [String] Resource identifier
|
|
28
|
+
attr_accessor :name
|
|
29
|
+
|
|
30
|
+
# @return [String, nil] Human-readable description
|
|
31
|
+
attr_accessor :description
|
|
32
|
+
|
|
33
|
+
# @return [String] URI pattern or URL
|
|
34
|
+
attr_accessor :uri
|
|
35
|
+
|
|
36
|
+
# @return [String, nil] MIME type of the resource content
|
|
37
|
+
attr_accessor :mime_type
|
|
38
|
+
|
|
39
|
+
def initialize(attributes = {})
|
|
40
|
+
attributes.each do |key, value|
|
|
41
|
+
setter = "#{key}="
|
|
42
|
+
send(setter, value) if respond_to?(setter)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Convert to hash representation
|
|
47
|
+
#
|
|
48
|
+
# @return [Hash]
|
|
49
|
+
def to_h
|
|
50
|
+
{
|
|
51
|
+
name: name,
|
|
52
|
+
description: description,
|
|
53
|
+
uri: uri,
|
|
54
|
+
mimeType: mime_type
|
|
55
|
+
}.compact
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Create from hash (flexible key formats)
|
|
59
|
+
#
|
|
60
|
+
# @param data [Hash]
|
|
61
|
+
# @return [Resource]
|
|
62
|
+
def self.from_hash(data)
|
|
63
|
+
new(
|
|
64
|
+
name: data["name"] || data[:name],
|
|
65
|
+
description: data["description"] || data[:description],
|
|
66
|
+
uri: data["uri"] || data[:uri],
|
|
67
|
+
mime_type: data["mimeType"] || data["mime_type"] || data[:mime_type] || data[:mimeType]
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Check if this is a file resource
|
|
72
|
+
#
|
|
73
|
+
# @return [Boolean]
|
|
74
|
+
def file?
|
|
75
|
+
uri&.start_with?("file://")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Check if this is an HTTP resource
|
|
79
|
+
#
|
|
80
|
+
# @return [Boolean]
|
|
81
|
+
def http?
|
|
82
|
+
uri&.match?(%r{\Ahttps?://})
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Check if resource is valid
|
|
86
|
+
#
|
|
87
|
+
# @return [Boolean]
|
|
88
|
+
def valid?
|
|
89
|
+
name.present? && uri.present?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Validation errors
|
|
93
|
+
#
|
|
94
|
+
# @return [Array<String>]
|
|
95
|
+
def validation_errors
|
|
96
|
+
errors = []
|
|
97
|
+
errors << "Resource must have a name" if name.blank?
|
|
98
|
+
errors << "Resource must have a uri" if uri.blank?
|
|
99
|
+
errors
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidAgent
|
|
4
|
+
module AgentManifest
|
|
5
|
+
# Tool represents a function/tool that an agent can invoke.
|
|
6
|
+
#
|
|
7
|
+
# Tools follow the MCP (Model Context Protocol) conventions for maximum
|
|
8
|
+
# portability across different LLM providers and frameworks.
|
|
9
|
+
#
|
|
10
|
+
# @example Inline tool definition
|
|
11
|
+
# tool = Tool.new(
|
|
12
|
+
# name: "search",
|
|
13
|
+
# description: "Search the web for information",
|
|
14
|
+
# input_schema: {
|
|
15
|
+
# "type" => "object",
|
|
16
|
+
# "properties" => {
|
|
17
|
+
# "query" => { "type" => "string", "description" => "Search query" }
|
|
18
|
+
# },
|
|
19
|
+
# "required" => ["query"]
|
|
20
|
+
# }
|
|
21
|
+
# )
|
|
22
|
+
#
|
|
23
|
+
# @example Tool reference
|
|
24
|
+
# tool = Tool.new(ref: "@activeagents/web-tools/search")
|
|
25
|
+
#
|
|
26
|
+
class Tool
|
|
27
|
+
# @return [String, nil] Tool name (required for inline definitions)
|
|
28
|
+
attr_accessor :name
|
|
29
|
+
|
|
30
|
+
# @return [String, nil] Human-readable description
|
|
31
|
+
attr_accessor :description
|
|
32
|
+
|
|
33
|
+
# @return [Hash, nil] JSON Schema for tool input parameters
|
|
34
|
+
attr_accessor :input_schema
|
|
35
|
+
|
|
36
|
+
# @return [String, nil] Reference to external tool (e.g., "$ref" or package reference)
|
|
37
|
+
attr_accessor :ref
|
|
38
|
+
|
|
39
|
+
def initialize(attributes = {})
|
|
40
|
+
attributes.each do |key, value|
|
|
41
|
+
setter = "#{key}="
|
|
42
|
+
send(setter, value) if respond_to?(setter)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Check if this tool is a reference to an external tool
|
|
47
|
+
#
|
|
48
|
+
# @return [Boolean]
|
|
49
|
+
def reference?
|
|
50
|
+
ref.present?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Check if this tool has an inline definition
|
|
54
|
+
#
|
|
55
|
+
# @return [Boolean]
|
|
56
|
+
def inline?
|
|
57
|
+
!reference? && name.present?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Convert to hash representation
|
|
61
|
+
#
|
|
62
|
+
# @return [Hash]
|
|
63
|
+
def to_h
|
|
64
|
+
if reference?
|
|
65
|
+
{ "$ref" => ref }
|
|
66
|
+
else
|
|
67
|
+
{
|
|
68
|
+
name: name,
|
|
69
|
+
description: description,
|
|
70
|
+
inputSchema: input_schema
|
|
71
|
+
}.compact
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Convert to MCP-compatible JSON string
|
|
76
|
+
#
|
|
77
|
+
# @return [String] JSON representation following MCP tool format
|
|
78
|
+
def to_mcp_json
|
|
79
|
+
{
|
|
80
|
+
name: name,
|
|
81
|
+
description: description,
|
|
82
|
+
inputSchema: input_schema
|
|
83
|
+
}.compact.to_json
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Convert to HasTools::ToolBuilder compatible schema
|
|
87
|
+
#
|
|
88
|
+
# This format is compatible with OpenAI's function calling API
|
|
89
|
+
# and SolidAgent's existing HasTools concern.
|
|
90
|
+
#
|
|
91
|
+
# @return [Hash]
|
|
92
|
+
def to_tool_builder_schema
|
|
93
|
+
{
|
|
94
|
+
type: "function",
|
|
95
|
+
name: name,
|
|
96
|
+
description: description,
|
|
97
|
+
parameters: input_schema || { type: "object", properties: {} }
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Create a Tool from a HasTools::ToolBuilder schema
|
|
102
|
+
#
|
|
103
|
+
# @param schema [Hash] Schema from ToolBuilder
|
|
104
|
+
# @return [Tool]
|
|
105
|
+
def self.from_tool_builder(schema)
|
|
106
|
+
new(
|
|
107
|
+
name: schema[:name] || schema["name"],
|
|
108
|
+
description: schema[:description] || schema["description"],
|
|
109
|
+
input_schema: schema[:parameters] || schema["parameters"]
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Create a Tool from a hash (flexible key formats)
|
|
114
|
+
#
|
|
115
|
+
# @param data [Hash] Tool data with various key formats
|
|
116
|
+
# @return [Tool]
|
|
117
|
+
def self.from_hash(data)
|
|
118
|
+
return new(ref: data["$ref"]) if data["$ref"]
|
|
119
|
+
|
|
120
|
+
new(
|
|
121
|
+
name: data["name"] || data[:name],
|
|
122
|
+
description: data["description"] || data[:description],
|
|
123
|
+
input_schema: data["inputSchema"] || data["input_schema"] || data[:input_schema] || data[:inputSchema]
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Get list of required parameters
|
|
128
|
+
#
|
|
129
|
+
# @return [Array<String>]
|
|
130
|
+
def required_parameters
|
|
131
|
+
input_schema&.dig("required") || input_schema&.dig(:required) || []
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Get all parameter names
|
|
135
|
+
#
|
|
136
|
+
# @return [Array<String>]
|
|
137
|
+
def parameter_names
|
|
138
|
+
properties = input_schema&.dig("properties") || input_schema&.dig(:properties) || {}
|
|
139
|
+
properties.keys.map(&:to_s)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Check if tool is valid (has required fields)
|
|
143
|
+
#
|
|
144
|
+
# @return [Boolean]
|
|
145
|
+
def valid?
|
|
146
|
+
reference? || name.present?
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Validation errors
|
|
150
|
+
#
|
|
151
|
+
# @return [Array<String>]
|
|
152
|
+
def validation_errors
|
|
153
|
+
errors = []
|
|
154
|
+
errors << "Tool must have a name or $ref" unless valid?
|
|
155
|
+
errors << "Tool description is recommended" if inline? && description.blank?
|
|
156
|
+
errors
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|