simple_acp 0.0.1

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 (80) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/CHANGELOG.md +5 -0
  4. data/COMMITS.md +196 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +385 -0
  7. data/Rakefile +13 -0
  8. data/docs/api/client-base.md +383 -0
  9. data/docs/api/index.md +159 -0
  10. data/docs/api/models.md +286 -0
  11. data/docs/api/server-base.md +379 -0
  12. data/docs/api/storage.md +347 -0
  13. data/docs/assets/images/simple_acp.jpg +0 -0
  14. data/docs/client/index.md +279 -0
  15. data/docs/client/sessions.md +324 -0
  16. data/docs/client/streaming.md +345 -0
  17. data/docs/client/sync-async.md +308 -0
  18. data/docs/core-concepts/agents.md +253 -0
  19. data/docs/core-concepts/events.md +337 -0
  20. data/docs/core-concepts/index.md +147 -0
  21. data/docs/core-concepts/messages.md +211 -0
  22. data/docs/core-concepts/runs.md +278 -0
  23. data/docs/core-concepts/sessions.md +281 -0
  24. data/docs/examples.md +659 -0
  25. data/docs/getting-started/configuration.md +166 -0
  26. data/docs/getting-started/index.md +62 -0
  27. data/docs/getting-started/installation.md +95 -0
  28. data/docs/getting-started/quick-start.md +189 -0
  29. data/docs/index.md +119 -0
  30. data/docs/server/creating-agents.md +360 -0
  31. data/docs/server/http-endpoints.md +411 -0
  32. data/docs/server/index.md +218 -0
  33. data/docs/server/multi-turn.md +329 -0
  34. data/docs/server/streaming.md +315 -0
  35. data/docs/storage/custom.md +414 -0
  36. data/docs/storage/index.md +176 -0
  37. data/docs/storage/memory.md +198 -0
  38. data/docs/storage/postgresql.md +350 -0
  39. data/docs/storage/redis.md +287 -0
  40. data/examples/01_basic/client.rb +88 -0
  41. data/examples/01_basic/server.rb +100 -0
  42. data/examples/02_async_execution/client.rb +107 -0
  43. data/examples/02_async_execution/server.rb +56 -0
  44. data/examples/03_run_management/client.rb +115 -0
  45. data/examples/03_run_management/server.rb +84 -0
  46. data/examples/04_rich_messages/client.rb +160 -0
  47. data/examples/04_rich_messages/server.rb +180 -0
  48. data/examples/05_await_resume/client.rb +164 -0
  49. data/examples/05_await_resume/server.rb +114 -0
  50. data/examples/06_agent_metadata/client.rb +188 -0
  51. data/examples/06_agent_metadata/server.rb +192 -0
  52. data/examples/README.md +252 -0
  53. data/examples/run_demo.sh +137 -0
  54. data/lib/simple_acp/client/base.rb +448 -0
  55. data/lib/simple_acp/client/sse.rb +141 -0
  56. data/lib/simple_acp/models/agent_manifest.rb +129 -0
  57. data/lib/simple_acp/models/await.rb +123 -0
  58. data/lib/simple_acp/models/base.rb +147 -0
  59. data/lib/simple_acp/models/errors.rb +102 -0
  60. data/lib/simple_acp/models/events.rb +256 -0
  61. data/lib/simple_acp/models/message.rb +235 -0
  62. data/lib/simple_acp/models/message_part.rb +225 -0
  63. data/lib/simple_acp/models/metadata.rb +161 -0
  64. data/lib/simple_acp/models/run.rb +298 -0
  65. data/lib/simple_acp/models/session.rb +137 -0
  66. data/lib/simple_acp/models/types.rb +210 -0
  67. data/lib/simple_acp/server/agent.rb +116 -0
  68. data/lib/simple_acp/server/app.rb +264 -0
  69. data/lib/simple_acp/server/base.rb +510 -0
  70. data/lib/simple_acp/server/context.rb +210 -0
  71. data/lib/simple_acp/server/falcon_runner.rb +61 -0
  72. data/lib/simple_acp/storage/base.rb +129 -0
  73. data/lib/simple_acp/storage/memory.rb +108 -0
  74. data/lib/simple_acp/storage/postgresql.rb +233 -0
  75. data/lib/simple_acp/storage/redis.rb +178 -0
  76. data/lib/simple_acp/version.rb +5 -0
  77. data/lib/simple_acp.rb +91 -0
  78. data/mkdocs.yml +152 -0
  79. data/sig/simple_acp.rbs +4 -0
  80. metadata +418 -0
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleAcp
4
+ module Models
5
+ # Fundamental communication structure in ACP.
6
+ #
7
+ # Messages represent units of communication between users and agents,
8
+ # containing one or more parts (text, JSON, images, etc.).
9
+ #
10
+ # @example Creating a simple text message
11
+ # msg = Message.user("Hello, world!")
12
+ #
13
+ # @example Creating a message with multiple parts
14
+ # msg = Message.agent(
15
+ # MessagePart.text("Here's the data:"),
16
+ # MessagePart.json({ count: 42 })
17
+ # )
18
+ class Message < Base
19
+ # @!attribute [r] role
20
+ # @return [String] "user" or "agent" (or "agent/name")
21
+ attribute :role, required: true
22
+
23
+ # @!attribute [r] parts
24
+ # @return [Array<MessagePart>] content parts
25
+ attribute :parts, default: -> { [] }
26
+
27
+ # @!attribute [r] created_at
28
+ # @return [Time, nil] when the message was created
29
+ attribute :created_at
30
+
31
+ # @!attribute [r] completed_at
32
+ # @return [Time, nil] when the message was completed
33
+ attribute :completed_at
34
+
35
+ def initialize(**kwargs)
36
+ super
37
+ @parts ||= []
38
+ @created_at ||= Time.now
39
+ end
40
+
41
+ # Create from a hash (JSON deserialization).
42
+ #
43
+ # @param hash [Hash, nil] message data
44
+ # @return [Message, nil] the message or nil
45
+ def self.from_hash(hash)
46
+ return nil if hash.nil?
47
+
48
+ instance = allocate
49
+ instance.send(:initialize_from_hash, hash)
50
+ instance
51
+ end
52
+
53
+ # Create a user message from content.
54
+ #
55
+ # @param contents [Array<String, MessagePart, Hash>] message content
56
+ # @return [Message] the user message
57
+ #
58
+ # @example
59
+ # Message.user("Hello!")
60
+ # Message.user(MessagePart.text("Hello"), MessagePart.json({key: "value"}))
61
+ def self.user(*contents)
62
+ parts = contents.map do |content|
63
+ case content
64
+ when MessagePart
65
+ content
66
+ when String
67
+ MessagePart.text(content)
68
+ when Hash
69
+ MessagePart.from_hash(content)
70
+ else
71
+ MessagePart.json(content)
72
+ end
73
+ end
74
+
75
+ new(role: Types::Role::USER, parts: parts)
76
+ end
77
+
78
+ # Create an agent message from content.
79
+ #
80
+ # @param contents [Array<String, MessagePart, Hash>] message content
81
+ # @return [Message] the agent message
82
+ #
83
+ # @example
84
+ # Message.agent("Hello, I'm your assistant!")
85
+ def self.agent(*contents)
86
+ parts = contents.map do |content|
87
+ case content
88
+ when MessagePart
89
+ content
90
+ when String
91
+ MessagePart.text(content)
92
+ when Hash
93
+ MessagePart.from_hash(content)
94
+ else
95
+ MessagePart.json(content)
96
+ end
97
+ end
98
+
99
+ new(role: Types::Role::AGENT, parts: parts)
100
+ end
101
+
102
+ # Check if this is a user message.
103
+ #
104
+ # @return [Boolean] true if role is "user"
105
+ def user?
106
+ @role == Types::Role::USER
107
+ end
108
+
109
+ # Check if this is an agent message.
110
+ #
111
+ # @return [Boolean] true if role is "agent" or starts with "agent/"
112
+ def agent?
113
+ @role == Types::Role::AGENT || @role.to_s.start_with?("agent/")
114
+ end
115
+
116
+ # Get the agent name if this is a named agent message.
117
+ #
118
+ # @return [String, nil] agent name extracted from "agent/name" role
119
+ def agent_name
120
+ return nil unless agent?
121
+
122
+ if @role.to_s.start_with?("agent/")
123
+ @role.to_s.sub("agent/", "")
124
+ end
125
+ end
126
+
127
+ # Add a part to this message.
128
+ #
129
+ # @param part [MessagePart, Hash] the part to add
130
+ # @return [self] for chaining
131
+ def add_part(part)
132
+ @parts << (part.is_a?(MessagePart) ? part : MessagePart.from_hash(part))
133
+ self
134
+ end
135
+
136
+ # Mark the message as completed.
137
+ #
138
+ # @return [self] for chaining
139
+ def complete!
140
+ @completed_at = Time.now
141
+ self
142
+ end
143
+
144
+ # Check if the message is completed.
145
+ #
146
+ # @return [Boolean] true if completed_at is set
147
+ def completed?
148
+ !@completed_at.nil?
149
+ end
150
+
151
+ # Combine two messages by appending parts.
152
+ #
153
+ # @param other [Message] message to append
154
+ # @return [Message] new combined message
155
+ def +(other)
156
+ combined = self.class.new(role: @role, parts: @parts.dup)
157
+ other.parts.each { |p| combined.add_part(p) }
158
+ combined
159
+ end
160
+
161
+ # Get combined text content from all text parts.
162
+ #
163
+ # @return [String] concatenated text content
164
+ def text_content
165
+ @parts.select(&:text?).map(&:content).join("\n")
166
+ end
167
+
168
+ # Create a new message with adjacent text parts combined.
169
+ #
170
+ # @return [Message] compressed message
171
+ def compress
172
+ return self if @parts.length <= 1
173
+
174
+ compressed_parts = []
175
+ current_text = nil
176
+
177
+ @parts.each do |part|
178
+ if part.text? && !part.base64_encoded?
179
+ if current_text
180
+ current_text = MessagePart.text("#{current_text.content}\n#{part.content}")
181
+ else
182
+ current_text = part.dup
183
+ end
184
+ else
185
+ compressed_parts << current_text if current_text
186
+ current_text = nil
187
+ compressed_parts << part
188
+ end
189
+ end
190
+
191
+ compressed_parts << current_text if current_text
192
+
193
+ self.class.new(role: @role, parts: compressed_parts, created_at: @created_at)
194
+ end
195
+
196
+ # Validate the message.
197
+ #
198
+ # @return [Boolean] true if role is valid, has parts, and all parts are valid
199
+ def valid?
200
+ return false unless Types::Role.valid?(@role)
201
+ return false if @parts.empty?
202
+ return false unless @parts.all?(&:valid?)
203
+
204
+ true
205
+ end
206
+
207
+ # Convert to string representation.
208
+ #
209
+ # @return [String] concatenated string representation of all parts
210
+ def to_s
211
+ @parts.map(&:to_s).join("\n")
212
+ end
213
+
214
+ private
215
+
216
+ def initialize_from_hash(hash)
217
+ @role = hash["role"] || hash[:role]
218
+ @created_at = parse_time(hash["created_at"] || hash[:created_at])
219
+ @completed_at = parse_time(hash["completed_at"] || hash[:completed_at])
220
+
221
+ parts_data = hash["parts"] || hash[:parts] || []
222
+ @parts = parts_data.map { |p| MessagePart.from_hash(p) }
223
+ end
224
+
225
+ def parse_time(value)
226
+ return nil if value.nil?
227
+ return value if value.is_a?(Time)
228
+
229
+ Time.parse(value.to_s)
230
+ rescue ArgumentError
231
+ nil
232
+ end
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+
5
+ module SimpleAcp
6
+ module Models
7
+ # Individual content unit within a Message.
8
+ #
9
+ # Message parts can contain text, JSON, images, or URL references.
10
+ # Each part has a content type (MIME type) and either inline content
11
+ # or a URL reference.
12
+ #
13
+ # @example Text part
14
+ # part = MessagePart.text("Hello, world!")
15
+ #
16
+ # @example JSON part
17
+ # part = MessagePart.json({ key: "value" })
18
+ #
19
+ # @example Image from base64
20
+ # part = MessagePart.image(base64_data, mime_type: "image/png")
21
+ #
22
+ # @example URL reference
23
+ # part = MessagePart.from_url("https://example.com/image.png", content_type: "image/png")
24
+ class MessagePart < Base
25
+ # @!attribute [r] name
26
+ # @return [String, nil] optional name for the part
27
+ attribute :name
28
+
29
+ # @!attribute [r] content_type
30
+ # @return [String] MIME type (e.g., "text/plain", "application/json")
31
+ attribute :content_type, required: true
32
+
33
+ # @!attribute [r] content
34
+ # @return [String, nil] inline content (mutually exclusive with content_url)
35
+ attribute :content
36
+
37
+ # @!attribute [r] content_encoding
38
+ # @return [String] "plain" or "base64"
39
+ attribute :content_encoding, default: Types::ContentEncoding::PLAIN
40
+
41
+ # @!attribute [r] content_url
42
+ # @return [String, nil] URL reference (mutually exclusive with content)
43
+ attribute :content_url
44
+
45
+ # @!attribute [r] metadata
46
+ # @return [CitationMetadata, TrajectoryMetadata, nil] optional metadata
47
+ attribute :metadata
48
+
49
+ def initialize(**kwargs)
50
+ super
51
+ validate!
52
+ end
53
+
54
+ # Create from a hash (JSON deserialization).
55
+ #
56
+ # @param hash [Hash, nil] part data
57
+ # @return [MessagePart, nil] the part or nil
58
+ def self.from_hash(hash)
59
+ return nil if hash.nil?
60
+
61
+ instance = allocate
62
+ instance.send(:initialize_from_hash, hash)
63
+ instance
64
+ end
65
+
66
+ # Create a plain text message part.
67
+ #
68
+ # @param content [String] the text content
69
+ # @param name [String, nil] optional name
70
+ # @return [MessagePart] the text part
71
+ def self.text(content, name: nil)
72
+ new(
73
+ content_type: "text/plain",
74
+ content: content,
75
+ name: name
76
+ )
77
+ end
78
+
79
+ # Create a JSON message part.
80
+ #
81
+ # @param data [Hash, Array, String] JSON data (will be serialized if not string)
82
+ # @param name [String, nil] optional name
83
+ # @return [MessagePart] the JSON part
84
+ def self.json(data, name: nil)
85
+ new(
86
+ content_type: "application/json",
87
+ content: data.is_a?(String) ? data : data.to_json,
88
+ name: name
89
+ )
90
+ end
91
+
92
+ # Create an image message part from base64 data.
93
+ #
94
+ # @param data [String] base64-encoded image data
95
+ # @param mime_type [String] image MIME type (default: "image/png")
96
+ # @param name [String, nil] optional name
97
+ # @return [MessagePart] the image part
98
+ def self.image(data, mime_type: "image/png", name: nil)
99
+ new(
100
+ content_type: mime_type,
101
+ content: data,
102
+ content_encoding: Types::ContentEncoding::BASE64,
103
+ name: name
104
+ )
105
+ end
106
+
107
+ # Create a message part referencing a URL.
108
+ #
109
+ # @param url [String] the URL to reference
110
+ # @param content_type [String] the content type at the URL
111
+ # @param name [String, nil] optional name
112
+ # @return [MessagePart] the URL reference part
113
+ def self.from_url(url, content_type:, name: nil)
114
+ new(
115
+ content_type: content_type,
116
+ content_url: url,
117
+ name: name
118
+ )
119
+ end
120
+
121
+ # Check if this is a text content part.
122
+ #
123
+ # @return [Boolean] true if content_type starts with "text/"
124
+ def text?
125
+ @content_type&.start_with?("text/")
126
+ end
127
+
128
+ # Check if this is a JSON content part.
129
+ #
130
+ # @return [Boolean] true if content_type is "application/json"
131
+ def json?
132
+ @content_type == "application/json"
133
+ end
134
+
135
+ # Check if this is an image content part.
136
+ #
137
+ # @return [Boolean] true if content_type starts with "image/"
138
+ def image?
139
+ @content_type&.start_with?("image/")
140
+ end
141
+
142
+ # Check if content is base64 encoded.
143
+ #
144
+ # @return [Boolean] true if content_encoding is "base64"
145
+ def base64_encoded?
146
+ @content_encoding == Types::ContentEncoding::BASE64
147
+ end
148
+
149
+ # Check if this part references a URL.
150
+ #
151
+ # @return [Boolean] true if content_url is set
152
+ def url_reference?
153
+ !@content_url.nil?
154
+ end
155
+
156
+ # Get decoded content (decodes base64 if needed).
157
+ #
158
+ # @return [String] decoded content
159
+ def decoded_content
160
+ return @content unless base64_encoded?
161
+
162
+ Base64.decode64(@content)
163
+ end
164
+
165
+ # Parse JSON content into Ruby objects.
166
+ #
167
+ # @return [Hash, Array, nil] parsed JSON or nil if not JSON
168
+ def parsed_json
169
+ return nil unless json?
170
+
171
+ JSON.parse(@content)
172
+ end
173
+
174
+ # Validate the message part.
175
+ #
176
+ # @return [Boolean] true if content_type is set and has content or URL (not both)
177
+ def valid?
178
+ return false if @content_type.nil?
179
+ return false if @content.nil? && @content_url.nil?
180
+ return false if @content && @content_url
181
+
182
+ true
183
+ end
184
+
185
+ # Convert to string representation.
186
+ #
187
+ # @return [String] content for text, URL for references, or "<type>" placeholder
188
+ def to_s
189
+ return @content if text?
190
+ return @content_url if url_reference?
191
+
192
+ "<#{@content_type}>"
193
+ end
194
+
195
+ private
196
+
197
+ def initialize_from_hash(hash)
198
+ @name = hash["name"] || hash[:name]
199
+ @content_type = hash["content_type"] || hash[:content_type]
200
+ @content = hash["content"] || hash[:content]
201
+ @content_encoding = hash["content_encoding"] || hash[:content_encoding] || Types::ContentEncoding::PLAIN
202
+ @content_url = hash["content_url"] || hash[:content_url]
203
+
204
+ metadata_hash = hash["metadata"] || hash[:metadata]
205
+ if metadata_hash
206
+ kind = metadata_hash["kind"] || metadata_hash[:kind]
207
+ @metadata = case kind
208
+ when "citation"
209
+ CitationMetadata.from_hash(metadata_hash)
210
+ when "trajectory"
211
+ TrajectoryMetadata.from_hash(metadata_hash)
212
+ end
213
+ end
214
+
215
+ validate!
216
+ end
217
+
218
+ def validate!
219
+ if @content && @content_url
220
+ raise SimpleAcp::ValidationError, "MessagePart cannot have both content and content_url"
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleAcp
4
+ module Models
5
+ # Citation metadata for source attribution
6
+ class CitationMetadata < Base
7
+ attribute :kind, default: "citation"
8
+ attribute :start_index
9
+ attribute :end_index
10
+ attribute :url
11
+ attribute :title
12
+ attribute :description
13
+
14
+ def self.from_hash(hash)
15
+ return nil if hash.nil?
16
+ return nil unless hash["kind"] == "citation" || hash[:kind] == "citation"
17
+
18
+ super
19
+ end
20
+ end
21
+
22
+ # Trajectory metadata for tracking multi-step reasoning
23
+ class TrajectoryMetadata < Base
24
+ attribute :kind, default: "trajectory"
25
+ attribute :message
26
+ attribute :tool_name
27
+ attribute :tool_input
28
+ attribute :tool_output
29
+
30
+ def self.from_hash(hash)
31
+ return nil if hash.nil?
32
+ return nil unless hash["kind"] == "trajectory" || hash[:kind] == "trajectory"
33
+
34
+ super
35
+ end
36
+ end
37
+
38
+ # Author information
39
+ class Author < Base
40
+ attribute :name, required: true
41
+ attribute :email
42
+ attribute :url
43
+ end
44
+
45
+ # Contributor information (same structure as Author)
46
+ class Contributor < Base
47
+ attribute :name, required: true
48
+ attribute :email
49
+ attribute :url
50
+ end
51
+
52
+ # Link to external resources
53
+ class Link < Base
54
+ attribute :type, required: true
55
+ attribute :url, required: true
56
+
57
+ def valid?
58
+ Types::LinkType::ALL.include?(@type) && Types.valid_url?(@url)
59
+ end
60
+ end
61
+
62
+ # Dependency declaration
63
+ class Dependency < Base
64
+ attribute :type, required: true
65
+ attribute :name, required: true
66
+
67
+ def valid?
68
+ Types::DependencyType::ALL.include?(@type)
69
+ end
70
+ end
71
+
72
+ # Agent capability
73
+ class Capability < Base
74
+ attribute :name, required: true
75
+ attribute :description, required: true
76
+ end
77
+
78
+ # Annotations for platform-specific metadata
79
+ class Annotations < Base
80
+ attribute :beeai_ui
81
+ attribute :extra, default: -> { {} }
82
+
83
+ def [](key)
84
+ @extra[key]
85
+ end
86
+
87
+ def []=(key, value)
88
+ @extra[key] = value
89
+ end
90
+
91
+ def to_h
92
+ hash = super
93
+ @extra&.each { |k, v| hash[k] = v }
94
+ hash
95
+ end
96
+ end
97
+
98
+ # Agent status metrics
99
+ class AgentStatus < Base
100
+ attribute :average_tokens
101
+ attribute :average_run_time
102
+ attribute :success_rate
103
+ end
104
+
105
+ # Full agent metadata
106
+ class Metadata < Base
107
+ attribute :annotations
108
+ attribute :documentation
109
+ attribute :license
110
+ attribute :programming_language
111
+ attribute :natural_languages, default: -> { [] }
112
+ attribute :framework
113
+ attribute :capabilities, default: -> { [] }
114
+ attribute :domains, default: -> { [] }
115
+ attribute :tags, default: -> { [] }
116
+ attribute :created_at
117
+ attribute :updated_at
118
+ attribute :author
119
+ attribute :contributors, default: -> { [] }
120
+ attribute :links, default: -> { [] }
121
+ attribute :dependencies, default: -> { [] }
122
+ attribute :recommended_models, default: -> { [] }
123
+
124
+ def self.from_hash(hash)
125
+ return nil if hash.nil?
126
+
127
+ instance = super
128
+
129
+ if hash["annotations"] || hash[:annotations]
130
+ instance.annotations = Annotations.from_hash(hash["annotations"] || hash[:annotations])
131
+ end
132
+
133
+ if hash["author"] || hash[:author]
134
+ instance.author = Author.from_hash(hash["author"] || hash[:author])
135
+ end
136
+
137
+ if hash["contributors"] || hash[:contributors]
138
+ contributors = hash["contributors"] || hash[:contributors]
139
+ instance.contributors = contributors.map { |c| Contributor.from_hash(c) }
140
+ end
141
+
142
+ if hash["capabilities"] || hash[:capabilities]
143
+ capabilities = hash["capabilities"] || hash[:capabilities]
144
+ instance.capabilities = capabilities.map { |c| Capability.from_hash(c) }
145
+ end
146
+
147
+ if hash["links"] || hash[:links]
148
+ links = hash["links"] || hash[:links]
149
+ instance.links = links.map { |l| Link.from_hash(l) }
150
+ end
151
+
152
+ if hash["dependencies"] || hash[:dependencies]
153
+ dependencies = hash["dependencies"] || hash[:dependencies]
154
+ instance.dependencies = dependencies.map { |d| Dependency.from_hash(d) }
155
+ end
156
+
157
+ instance
158
+ end
159
+ end
160
+ end
161
+ end