llm.rb 10.0.0 → 11.1.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +158 -10
  3. data/README.md +145 -44
  4. data/lib/llm/a2a/card/capabilities.rb +41 -0
  5. data/lib/llm/a2a/card/interface.rb +34 -0
  6. data/lib/llm/a2a/card/provider.rb +27 -0
  7. data/lib/llm/a2a/card/skill.rb +68 -0
  8. data/lib/llm/a2a/card.rb +144 -0
  9. data/lib/llm/a2a/error.rb +49 -0
  10. data/lib/llm/a2a/notifications.rb +53 -0
  11. data/lib/llm/a2a/tasks.rb +55 -0
  12. data/lib/llm/a2a/transport/http.rb +131 -0
  13. data/lib/llm/a2a.rb +452 -0
  14. data/lib/llm/active_record/acts_as_agent.rb +15 -9
  15. data/lib/llm/active_record/acts_as_llm.rb +4 -4
  16. data/lib/llm/agent.rb +15 -9
  17. data/lib/llm/buffer.rb +1 -2
  18. data/lib/llm/context.rb +52 -5
  19. data/lib/llm/file.rb +7 -0
  20. data/lib/llm/function.rb +13 -5
  21. data/lib/llm/mcp/transport/http.rb +5 -18
  22. data/lib/llm/mcp/transport/stdio.rb +7 -0
  23. data/lib/llm/mcp.rb +20 -17
  24. data/lib/llm/message.rb +1 -1
  25. data/lib/llm/object/builder.rb +1 -0
  26. data/lib/llm/object/kernel.rb +1 -1
  27. data/lib/llm/object.rb +9 -0
  28. data/lib/llm/provider.rb +2 -9
  29. data/lib/llm/response.rb +1 -1
  30. data/lib/llm/schema.rb +23 -5
  31. data/lib/llm/sequel/agent.rb +14 -9
  32. data/lib/llm/sequel/plugin.rb +8 -7
  33. data/lib/llm/skill.rb +44 -14
  34. data/lib/llm/tool.rb +57 -27
  35. data/lib/llm/tracer/telemetry.rb +3 -1
  36. data/lib/llm/tracer.rb +1 -1
  37. data/lib/llm/transport/http.rb +1 -1
  38. data/lib/llm/transport/stream_decoder.rb +6 -3
  39. data/lib/llm/transport/utils.rb +35 -0
  40. data/lib/llm/transport.rb +1 -0
  41. data/lib/llm/utils.rb +44 -0
  42. data/lib/llm/version.rb +1 -1
  43. data/lib/llm.rb +23 -4
  44. data/llm.gemspec +16 -1
  45. metadata +26 -3
  46. data/lib/llm/mcp/transport/http/event_handler.rb +0 -68
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::A2A::Card
4
+ ##
5
+ # Represents a single skill/capability of an agent.
6
+ class Skill
7
+ ##
8
+ # @param [Hash] data
9
+ def initialize(data)
10
+ @data = LLM::Object.from(data)
11
+ end
12
+
13
+ ##
14
+ # Returns the unique identifier for the skill.
15
+ # @return [String]
16
+ def id
17
+ @data.id
18
+ end
19
+
20
+ ##
21
+ # Returns the human-readable skill name.
22
+ # @return [String]
23
+ def name
24
+ @data.name
25
+ end
26
+
27
+ ##
28
+ # Returns the detailed skill description.
29
+ # @return [String]
30
+ def description
31
+ @data.description
32
+ end
33
+
34
+ ##
35
+ # Returns capability tags for the skill.
36
+ # @return [Array<String>]
37
+ def tags
38
+ @data.tags || []
39
+ end
40
+
41
+ ##
42
+ # Returns example prompts for the skill.
43
+ # @return [Array<String>]
44
+ def examples
45
+ @data.examples || []
46
+ end
47
+
48
+ ##
49
+ # Returns the supported input media types.
50
+ # @return [Array<String>]
51
+ def input_modes
52
+ @data.inputModes || @data.input_modes || []
53
+ end
54
+
55
+ ##
56
+ # Returns the supported output media types.
57
+ # @return [Array<String>]
58
+ def output_modes
59
+ @data.outputModes || @data.output_modes || []
60
+ end
61
+
62
+ ##
63
+ # @return [String]
64
+ def inspect
65
+ "#<#{LLM::Utils.object_id(self)} @id=#{id.inspect} @name=#{name.inspect}>"
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::A2A
4
+ ##
5
+ # Represents an A2A Agent Card — a self-describing manifest for an
6
+ # agent that provides metadata including the agent's identity,
7
+ # capabilities, skills, supported communication methods, and security
8
+ # requirements.
9
+ #
10
+ # Agent Cards are published at `/.well-known/agent-card.json` and
11
+ # allow clients to discover an agent's capabilities before interacting.
12
+ #
13
+ # @example
14
+ # a2a = LLM::A2A.rest(url: "https://agent.example.com")
15
+ # card = a2a.card
16
+ # puts card.name # => "GeoSpatial Route Planner Agent"
17
+ # puts card.description # => "Provides advanced route planning..."
18
+ # card.skills.each { |s| puts "#{s.name}: #{s.description}" }
19
+ class Card
20
+ require_relative "card/skill"
21
+ require_relative "card/interface"
22
+ require_relative "card/capabilities"
23
+ require_relative "card/provider"
24
+
25
+ ##
26
+ # @param [Hash] data The raw Agent Card JSON data
27
+ def initialize(data)
28
+ @data = LLM::Object.from(data)
29
+ end
30
+
31
+ ##
32
+ # Returns a human-readable name for the agent.
33
+ # @return [String]
34
+ def name
35
+ @data.name
36
+ end
37
+
38
+ ##
39
+ # Returns a human-readable description of the agent.
40
+ # @return [String]
41
+ def description
42
+ @data.description
43
+ end
44
+
45
+ ##
46
+ # Returns the agent version.
47
+ # @return [String]
48
+ def version
49
+ @data.version
50
+ end
51
+
52
+ ##
53
+ # Returns the advertised A2A protocol version.
54
+ # @return [String, nil]
55
+ def protocol_version
56
+ @data.protocolVersion || @data.protocol_version
57
+ end
58
+
59
+ ##
60
+ # Returns the documentation URL, when present.
61
+ # @return [String, nil]
62
+ def documentation_url
63
+ @data.documentationUrl || @data.documentation_url
64
+ end
65
+
66
+ ##
67
+ # Returns the icon URL, when present.
68
+ # @return [String, nil]
69
+ def icon_url
70
+ @data.iconUrl || @data.icon_url
71
+ end
72
+
73
+ ##
74
+ # Returns the skills provided by the agent.
75
+ # @return [Array<LLM::A2A::Card::Skill>]
76
+ def skills
77
+ @skills ||= (@data.skills || []).map { Skill.new(_1) }
78
+ end
79
+
80
+ ##
81
+ # Returns the interfaces supported by the agent.
82
+ # @return [Array<LLM::A2A::Card::Interface>]
83
+ def interfaces
84
+ @interfaces ||= (@data.supportedInterfaces || @data.supported_interfaces || []).map { Interface.new(_1) }
85
+ end
86
+
87
+ ##
88
+ # Returns the optional capabilities declaration.
89
+ # @return [LLM::A2A::Card::Capabilities, nil]
90
+ def capabilities
91
+ raw = @data.capabilities
92
+ raw ? Capabilities.new(raw) : nil
93
+ end
94
+
95
+ ##
96
+ # Returns the agent card signatures.
97
+ # @return [Array<LLM::Object>]
98
+ def signatures
99
+ @signatures ||= (@data.signatures || []).map { LLM::Object.from(_1) }
100
+ end
101
+
102
+ ##
103
+ # Returns the security scheme definitions.
104
+ # @return [Hash<String, Hash>, nil]
105
+ def security_schemes
106
+ @data.securitySchemes || @data.security_schemes
107
+ end
108
+
109
+ ##
110
+ # Returns the security requirements.
111
+ # @return [Array<Hash>, nil]
112
+ def security_requirements
113
+ @data.security || @data.security_requirements
114
+ end
115
+
116
+ ##
117
+ # Returns the declared provider information.
118
+ # @return [LLM::A2A::Card::Provider, nil]
119
+ def provider
120
+ raw = @data.provider
121
+ raw ? Provider.new(raw) : nil
122
+ end
123
+
124
+ ##
125
+ # Returns the default input media types.
126
+ # @return [Array<String>]
127
+ def default_input_modes
128
+ @data.defaultInputModes || @data.default_input_modes || []
129
+ end
130
+
131
+ ##
132
+ # Returns the default output media types.
133
+ # @return [Array<String>]
134
+ def default_output_modes
135
+ @data.defaultOutputModes || @data.default_output_modes || []
136
+ end
137
+
138
+ ##
139
+ # @return [String]
140
+ def inspect
141
+ "#<#{LLM::Utils.object_id(self)} @name=#{name.inspect} @skills=#{skills.size}>"
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::A2A
4
+ ##
5
+ # Generic A2A protocol error.
6
+ Error = Class.new(LLM::Error) do
7
+ ##
8
+ # @return [Integer, nil]
9
+ attr_reader :code
10
+
11
+ ##
12
+ # @return [Object, nil]
13
+ attr_reader :data
14
+
15
+ ##
16
+ # @param [String] message
17
+ # @param [Integer, nil] code
18
+ # @param [Object, nil] data
19
+ def initialize(message, code = nil, data = nil)
20
+ super(message)
21
+ @code = code
22
+ @data = data
23
+ end
24
+ end
25
+
26
+ ##
27
+ # Raised when the agent card cannot be fetched or parsed.
28
+ AgentCardError = Class.new(Error)
29
+
30
+ ##
31
+ # Raised when a task is not found.
32
+ TaskNotFoundError = Class.new(Error)
33
+
34
+ ##
35
+ # Raised when a task cannot be cancelled.
36
+ TaskNotCancelableError = Class.new(Error)
37
+
38
+ ##
39
+ # Raised when the agent does not support the requested operation.
40
+ UnsupportedOperationError = Class.new(Error)
41
+
42
+ ##
43
+ # Raised when a content type is not supported.
44
+ ContentTypeNotSupportedError = Class.new(Error)
45
+
46
+ ##
47
+ # Raised when the A2A protocol version is not supported.
48
+ VersionNotSupportedError = Class.new(Error)
49
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::A2A
4
+ ##
5
+ # Groups push notification configuration operations.
6
+ class Notifications
7
+ ##
8
+ # @param [LLM::A2A] a2a
9
+ def initialize(a2a)
10
+ @a2a = a2a
11
+ end
12
+
13
+ ##
14
+ # Creates a push notification configuration for a task.
15
+ # @param [String] task_id
16
+ # @param [String] url
17
+ # @param [String, nil] token
18
+ # @param [Hash, nil] authentication
19
+ # @param [String, nil] id
20
+ # @return [LLM::Object]
21
+ def create(task_id, url:, token: nil, authentication: nil, id: nil)
22
+ @a2a.create_task_push_notification_config(task_id, url:, token:, authentication:, id:)
23
+ end
24
+
25
+ ##
26
+ # Retrieves a push notification configuration for a task.
27
+ # @param [String] task_id
28
+ # @param [String] id
29
+ # @return [LLM::Object]
30
+ def get(task_id, id)
31
+ @a2a.get_task_push_notification_config(task_id, id)
32
+ end
33
+
34
+ ##
35
+ # Lists push notification configurations for a task.
36
+ # @param [String] task_id
37
+ # @param [Integer, nil] page_size
38
+ # @param [String, nil] page_token
39
+ # @return [LLM::Object]
40
+ def list(task_id, page_size: nil, page_token: nil)
41
+ @a2a.list_task_push_notification_configs(task_id, page_size:, page_token:)
42
+ end
43
+
44
+ ##
45
+ # Deletes a push notification configuration for a task.
46
+ # @param [String] task_id
47
+ # @param [String] id
48
+ # @return [LLM::Object]
49
+ def delete(task_id, id)
50
+ @a2a.delete_task_push_notification_config(task_id, id)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::A2A
4
+ ##
5
+ # Groups task-oriented A2A operations.
6
+ class Tasks
7
+ ##
8
+ # @param [LLM::A2A] a2a
9
+ def initialize(a2a)
10
+ @a2a = a2a
11
+ end
12
+
13
+ ##
14
+ # Returns the current state of a task.
15
+ # @param [String] task_id
16
+ # @param [Integer, nil] history_length
17
+ # @return [LLM::Object]
18
+ def get(task_id, history_length: nil)
19
+ @a2a.get_task(task_id, history_length:)
20
+ end
21
+
22
+ ##
23
+ # Lists tasks with optional filtering.
24
+ # @param [String, nil] context_id
25
+ # @param [String, nil] status
26
+ # @param [Integer, nil] history_length
27
+ # @param [String, nil] status_timestamp_after
28
+ # @param [Boolean, nil] include_artifacts
29
+ # @param [Integer] page_size
30
+ # @param [String, nil] page_token
31
+ # @return [LLM::Object]
32
+ def list(context_id: nil, status: nil, history_length: nil, status_timestamp_after: nil,
33
+ include_artifacts: nil, page_size: 20, page_token: nil)
34
+ @a2a.list_tasks(context_id:, status:, history_length:, status_timestamp_after:,
35
+ include_artifacts:, page_size:, page_token:)
36
+ end
37
+
38
+ ##
39
+ # Cancels a task in progress.
40
+ # @param [String] task_id
41
+ # @return [LLM::Object]
42
+ def cancel(task_id, metadata: nil)
43
+ @a2a.cancel_task(task_id, metadata:)
44
+ end
45
+
46
+ ##
47
+ # Subscribes to streaming updates for an existing task.
48
+ # @param [String] task_id
49
+ # @yieldparam [LLM::Object] event
50
+ # @return [void]
51
+ def subscribe(task_id, &on_event)
52
+ @a2a.subscribe_to_task(task_id, &on_event)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::A2A
4
+ module Transport
5
+ ##
6
+ # The {LLM::A2A::Transport::HTTP} class provides the HTTP+JSON/REST
7
+ # protocol binding for the A2A protocol. It sends JSON payloads to
8
+ # A2A agent endpoints and handles both synchronous responses and
9
+ # Server-Sent Events for streaming operations.
10
+ #
11
+ # This transport implements the HTTP+JSON/REST binding as defined
12
+ # in the A2A specification (Section 11).
13
+ class HTTP
14
+ include LLM::Transport::Utils
15
+
16
+ ##
17
+ # @param [String] url The base URL of the A2A agent
18
+ # @param [Hash<String, String>] headers Extra HTTP headers
19
+ # @param [Integer, nil] timeout The timeout in seconds
20
+ # @param [LLM::Transport, Class, nil] transport Override transport
21
+ # @param [String] protocol_version The A2A protocol version header
22
+ def initialize(url:, headers: {}, timeout: nil, transport: nil, protocol_version: "1.0")
23
+ @uri = URI.parse(url)
24
+ @headers = headers
25
+ @protocol_version = protocol_version
26
+ @transport = resolve_transport(@uri, transport, timeout)
27
+ end
28
+
29
+ ##
30
+ # Sends a GET request.
31
+ # @param [String] path The URL path
32
+ # @return [Hash]
33
+ def get(path, accept: "application/json")
34
+ req = Net::HTTP::Get.new(request_path(path), headers(accept:))
35
+ res = transport.request(req, owner: self)
36
+ parse_response(res)
37
+ end
38
+
39
+ ##
40
+ # Sends a POST request.
41
+ # @param [String] path The URL path
42
+ # @param [Hash] body The JSON body
43
+ # @return [Hash]
44
+ def post(path, body, content_type: "application/json", accept: "application/json")
45
+ req = Net::HTTP::Post.new(request_path(path), headers(content_type:, accept:))
46
+ req.body = LLM.json.dump(body)
47
+ res = transport.request(req, owner: self)
48
+ parse_response(res)
49
+ end
50
+
51
+ ##
52
+ # Sends a DELETE request.
53
+ # @param [String] path The URL path
54
+ # @return [Hash]
55
+ def delete(path, accept: "application/json")
56
+ req = Net::HTTP::Delete.new(request_path(path), headers(accept:))
57
+ res = transport.request(req, owner: self)
58
+ parse_response(res)
59
+ end
60
+
61
+ ##
62
+ # Sends a GET request with a streaming (SSE) response.
63
+ #
64
+ # The block is called for each event parsed from the event stream.
65
+ # @param [String] path The URL path
66
+ # @yieldparam [LLM::Object] event A stream event
67
+ # @return [void]
68
+ def get_stream(path, &on_event)
69
+ req = Net::HTTP::Get.new(request_path(path), headers(accept: "text/event-stream"))
70
+ stream(req, &on_event)
71
+ end
72
+
73
+ ##
74
+ # Sends a POST request with a streaming (SSE) response.
75
+ #
76
+ # The block is called for each event parsed from the event stream.
77
+ # @param [String] path The URL path
78
+ # @param [Hash] body The JSON body
79
+ # @yieldparam [LLM::Object] event A stream event
80
+ # @return [void]
81
+ def post_stream(path, body, content_type: "application/json", &on_event)
82
+ req = Net::HTTP::Post.new(request_path(path), headers(content_type:, accept: "text/event-stream"))
83
+ req.body = LLM.json.dump(body)
84
+ stream(req, &on_event)
85
+ end
86
+
87
+ private
88
+
89
+ attr_reader :transport
90
+
91
+ def headers(content_type: "application/json", accept: "application/json")
92
+ {
93
+ "A2A-Version" => @protocol_version,
94
+ "content-type" => content_type,
95
+ "accept" => accept
96
+ }.merge(@headers)
97
+ end
98
+
99
+ def request_path(path)
100
+ path = LLM::Utils.normalize_base_path(path)
101
+ path.empty? ? "/" : path
102
+ end
103
+
104
+ def stream(req, &on_event)
105
+ transport.request(req, owner: self) do |raw|
106
+ raw = LLM::Transport::Response.from(raw)
107
+ next raw unless raw.success?
108
+ decoder = LLM::Transport::StreamDecoder.new(&on_event)
109
+ raw.read_body { decoder << _1 }
110
+ decoder.free
111
+ end
112
+ end
113
+
114
+ def parse_response(res)
115
+ res = LLM::Transport::Response.from(res)
116
+ body = res.body.to_s
117
+ if res.success?
118
+ body.empty? ? {} : LLM.json.load(body)
119
+ else
120
+ handle_error(res.code, body)
121
+ end
122
+ end
123
+
124
+ def handle_error(code, body)
125
+ data = LLM.json.load(body) rescue {}
126
+ msg = data.dig("error", "message") || data["message"] || "HTTP #{code}"
127
+ raise LLM::A2A::Error.new(msg, code.to_i, data)
128
+ end
129
+ end
130
+ end
131
+ end