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,114 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Await/Resume Example - Server
5
+ #
6
+ # Demonstrates the await/resume pattern for interactive multi-step flows.
7
+ # The agent can pause execution and request input from the client.
8
+ #
9
+ # Run with: ruby examples/05_await_resume/server.rb
10
+ # Then connect with: ruby examples/05_await_resume/client.rb
11
+
12
+ require_relative "../../lib/simple_acp"
13
+
14
+ server = SimpleAcp::Server::Base.new
15
+
16
+ # Simple single-question agent
17
+ server.agent("greeter",
18
+ description: "Asks for your name and greets you"
19
+ ) do |context|
20
+ if context.resume_message
21
+ # Resume: we have the client's response
22
+ name = context.resume_message.text_content
23
+ SimpleAcp::Models::Message.agent("Hello, #{name}! Nice to meet you!")
24
+ else
25
+ # Initial call: ask for input
26
+ context.await_message(
27
+ SimpleAcp::Models::Message.agent("What is your name?")
28
+ )
29
+ end
30
+ end
31
+
32
+ # Multi-step wizard using state to track progress
33
+ server.agent("survey",
34
+ description: "A multi-step survey that collects information"
35
+ ) do |context|
36
+ # Use state to track current step and collected data
37
+ survey_state = context.state || { step: 0, answers: {} }
38
+ step = survey_state[:step]
39
+
40
+ case step
41
+ when 0
42
+ # Step 1: Ask for name
43
+ context.set_state({ step: 1, answers: {} })
44
+ context.await_message(
45
+ SimpleAcp::Models::Message.agent("Welcome to the survey! Question 1: What is your name?")
46
+ )
47
+
48
+ when 1
49
+ # Step 2: Got name, ask for favorite color
50
+ answers = survey_state[:answers].merge("name" => context.resume_message&.text_content)
51
+ context.set_state({ step: 2, answers: answers })
52
+ context.await_message(
53
+ SimpleAcp::Models::Message.agent("Thanks #{answers['name']}! Question 2: What is your favorite color?")
54
+ )
55
+
56
+ when 2
57
+ # Step 3: Got color, ask for favorite number
58
+ answers = survey_state[:answers].merge("color" => context.resume_message&.text_content)
59
+ context.set_state({ step: 3, answers: answers })
60
+ context.await_message(
61
+ SimpleAcp::Models::Message.agent("Great choice! Question 3: What is your favorite number?")
62
+ )
63
+
64
+ when 3
65
+ # Final step: Got all answers, return summary
66
+ answers = survey_state[:answers].merge("number" => context.resume_message&.text_content)
67
+ context.set_state({ step: 0, answers: {} }) # Reset for next survey
68
+
69
+ summary = <<~SUMMARY
70
+ Survey complete! Here are your answers:
71
+ - Name: #{answers['name']}
72
+ - Favorite color: #{answers['color']}
73
+ - Favorite number: #{answers['number']}
74
+ Thank you for participating!
75
+ SUMMARY
76
+
77
+ SimpleAcp::Models::Message.agent(summary)
78
+ end
79
+ end
80
+
81
+ # Confirmation agent - asks for yes/no confirmation
82
+ server.agent("confirmer",
83
+ description: "Performs an action after confirmation"
84
+ ) do |context|
85
+ action = context.input.first&.text_content || "do something"
86
+ state = context.state || { confirmed: nil }
87
+
88
+ if state[:confirmed].nil?
89
+ # First call: ask for confirmation
90
+ context.set_state({ action: action, confirmed: false })
91
+ context.await_message(
92
+ SimpleAcp::Models::Message.agent("Are you sure you want to '#{action}'? (yes/no)")
93
+ )
94
+ else
95
+ # Resume: check confirmation
96
+ response = context.resume_message&.text_content&.downcase&.strip
97
+ context.set_state(nil) # Clear state
98
+
99
+ if %w[yes y sure ok].include?(response)
100
+ SimpleAcp::Models::Message.agent("Action '#{state[:action]}' confirmed and executed!")
101
+ else
102
+ SimpleAcp::Models::Message.agent("Action '#{state[:action]}' cancelled.")
103
+ end
104
+ end
105
+ end
106
+
107
+ puts "Starting Await/Resume Demo Server..."
108
+ puts "Available agents:"
109
+ server.agents.each do |name, agent|
110
+ puts " - #{name}: #{agent.description}"
111
+ end
112
+ puts
113
+
114
+ server.run(port: 8000)
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Agent Metadata Example - Client
5
+ #
6
+ # Demonstrates:
7
+ # - Retrieving full agent metadata
8
+ # - Inspecting capabilities, author, links
9
+ # - Content type negotiation
10
+ # - Using the agent(name) endpoint
11
+ #
12
+ # First start the server: ruby examples/06_agent_metadata/server.rb
13
+ # Then run this client: ruby examples/06_agent_metadata/client.rb
14
+
15
+ require_relative "../../lib/simple_acp"
16
+ require "json"
17
+
18
+ client = SimpleAcp::Client::Base.new(base_url: "http://localhost:8000")
19
+
20
+ puts "=== Agent Metadata Demo ==="
21
+ puts
22
+
23
+ # Verify server is running
24
+ unless client.ping
25
+ puts "Server is not responding"
26
+ exit 1
27
+ end
28
+ puts "Server is healthy"
29
+ puts
30
+
31
+ # --- Demo 1: List all agents with basic info ---
32
+ puts "--- Demo 1: List Agents Overview ---"
33
+ puts
34
+
35
+ agents_response = client.agents
36
+ puts "Found #{agents_response.agents.length} agents:"
37
+ puts
38
+ agents_response.agents.each do |agent|
39
+ puts "#{agent.name}"
40
+ puts " Description: #{agent.description}"
41
+ puts " Input types: #{agent.input_content_types.join(', ')}"
42
+ puts " Output types: #{agent.output_content_types.join(', ')}"
43
+ puts
44
+ end
45
+
46
+ # --- Demo 2: Get detailed agent metadata ---
47
+ puts "--- Demo 2: Detailed Agent Metadata ---"
48
+ puts
49
+
50
+ agent = client.agent("text-analyzer")
51
+
52
+ puts "Agent: #{agent.name}"
53
+ puts "Description: #{agent.description}"
54
+ puts
55
+
56
+ if agent.metadata
57
+ meta = agent.metadata
58
+
59
+ puts "Documentation:"
60
+ puts " #{meta.documentation}"
61
+ puts
62
+
63
+ if meta.author
64
+ puts "Author:"
65
+ puts " Name: #{meta.author.name}"
66
+ puts " Email: #{meta.author.email}" if meta.author.email
67
+ puts " URL: #{meta.author.url}" if meta.author.url
68
+ puts
69
+ end
70
+
71
+ if meta.contributors&.any?
72
+ puts "Contributors:"
73
+ meta.contributors.each do |contrib|
74
+ puts " - #{contrib.name}"
75
+ puts " Email: #{contrib.email}" if contrib.email
76
+ puts " URL: #{contrib.url}" if contrib.url
77
+ end
78
+ puts
79
+ end
80
+
81
+ if meta.capabilities&.any?
82
+ puts "Capabilities:"
83
+ meta.capabilities.each do |cap|
84
+ puts " - #{cap.name}: #{cap.description}"
85
+ end
86
+ puts
87
+ end
88
+
89
+ if meta.links&.any?
90
+ puts "Links:"
91
+ meta.links.each do |link|
92
+ puts " - #{link.type}: #{link.url}"
93
+ end
94
+ puts
95
+ end
96
+
97
+ if meta.dependencies&.any?
98
+ puts "Dependencies:"
99
+ meta.dependencies.each do |dep|
100
+ puts " - #{dep.type}: #{dep.name}"
101
+ end
102
+ puts
103
+ end
104
+
105
+ puts "Additional Info:"
106
+ puts " License: #{meta.license}" if meta.license
107
+ puts " Language: #{meta.programming_language}" if meta.programming_language
108
+ puts " Framework: #{meta.framework}" if meta.framework
109
+ puts " Natural languages: #{meta.natural_languages.join(', ')}" if meta.natural_languages&.any?
110
+ puts " Domains: #{meta.domains.join(', ')}" if meta.domains&.any?
111
+ puts " Tags: #{meta.tags.join(', ')}" if meta.tags&.any?
112
+ puts " Created: #{meta.created_at}" if meta.created_at
113
+ puts " Updated: #{meta.updated_at}" if meta.updated_at
114
+ puts " Recommended models: #{meta.recommended_models.join(', ')}" if meta.recommended_models&.any?
115
+ else
116
+ puts "(No detailed metadata)"
117
+ end
118
+ puts
119
+
120
+ # --- Demo 3: Content Type Negotiation ---
121
+ puts "--- Demo 3: Content Type Checking ---"
122
+ puts
123
+
124
+ agent = client.agent("text-analyzer")
125
+
126
+ test_types = ["text/plain", "text/markdown", "application/json", "image/png", "text/html"]
127
+
128
+ puts "Testing input content types for #{agent.name}:"
129
+ test_types.each do |type|
130
+ accepts = agent.accepts_content_type?(type)
131
+ puts " #{type}: #{accepts ? 'accepted' : 'rejected'}"
132
+ end
133
+ puts
134
+
135
+ puts "Testing output content types for #{agent.name}:"
136
+ test_types.each do |type|
137
+ produces = agent.produces_content_type?(type)
138
+ puts " #{type}: #{produces ? 'produced' : 'not produced'}"
139
+ end
140
+ puts
141
+
142
+ # --- Demo 4: Compare agents with different metadata levels ---
143
+ puts "--- Demo 4: Metadata Comparison ---"
144
+ puts
145
+
146
+ %w[text-analyzer simple-echo json-processor].each do |name|
147
+ agent = client.agent(name)
148
+ puts "#{name}:"
149
+ puts " Has metadata: #{agent.metadata ? 'yes' : 'no'}"
150
+ if agent.metadata
151
+ puts " Domains: #{agent.metadata.domains&.join(', ') || 'none'}"
152
+ puts " Tags: #{agent.metadata.tags&.join(', ') || 'none'}"
153
+ puts " Capabilities: #{agent.metadata.capabilities&.length || 0}"
154
+ puts " Links: #{agent.metadata.links&.length || 0}"
155
+ end
156
+ puts
157
+ end
158
+
159
+ # --- Demo 5: Using the text analyzer ---
160
+ puts "--- Demo 5: Using Text Analyzer Agent ---"
161
+ puts
162
+
163
+ sample_text = "Ruby is a wonderful programming language. It has great syntax and an amazing community. I love writing code in Ruby because it makes me happy and productive."
164
+
165
+ puts "Analyzing text:"
166
+ puts " \"#{sample_text[0..60]}...\""
167
+ puts
168
+
169
+ run = client.run_sync(agent: "text-analyzer", input: sample_text)
170
+
171
+ run.output.each do |message|
172
+ message.parts.each do |part|
173
+ case part.content_type
174
+ when "application/json"
175
+ data = JSON.parse(part.content)
176
+ puts "Results:"
177
+ puts " Word count: #{data['word_count']}"
178
+ puts " Sentiment: #{data['sentiment']}"
179
+ puts " Keywords: #{data['keywords'].join(', ')}"
180
+ puts " Summary: #{data['summary']}"
181
+ when "text/plain"
182
+ puts " #{part.content}"
183
+ end
184
+ end
185
+ end
186
+ puts
187
+
188
+ puts "=== Agent Metadata Demo Complete ==="
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Agent Metadata Example - Server
5
+ #
6
+ # Demonstrates rich agent metadata including:
7
+ # - Author and contributor information
8
+ # - Links to documentation and source code
9
+ # - Capabilities and domains
10
+ # - Dependencies
11
+ # - Content type negotiation
12
+ #
13
+ # Run with: ruby examples/06_agent_metadata/server.rb
14
+ # Then connect with: ruby examples/06_agent_metadata/client.rb
15
+
16
+ require_relative "../../lib/simple_acp"
17
+
18
+ server = SimpleAcp::Server::Base.new
19
+
20
+ # Agent with full metadata
21
+ metadata = SimpleAcp::Models::Metadata.new(
22
+ documentation: "A comprehensive text analysis agent that can summarize, analyze sentiment, and extract keywords from text.",
23
+ license: "MIT",
24
+ programming_language: "ruby",
25
+ natural_languages: ["en", "es", "fr"],
26
+ framework: "SimpleAcp",
27
+ created_at: "2024-01-15T10:00:00Z",
28
+ updated_at: "2024-06-20T14:30:00Z",
29
+ author: SimpleAcp::Models::Author.new(
30
+ name: "Alice Developer",
31
+ email: "alice@example.com",
32
+ url: "https://github.com/alicedev"
33
+ ),
34
+ contributors: [
35
+ SimpleAcp::Models::Contributor.new(
36
+ name: "Bob Contributor",
37
+ email: "bob@example.com"
38
+ ),
39
+ SimpleAcp::Models::Contributor.new(
40
+ name: "Charlie Helper",
41
+ url: "https://github.com/charliehelper"
42
+ )
43
+ ],
44
+ capabilities: [
45
+ SimpleAcp::Models::Capability.new(
46
+ name: "summarize",
47
+ description: "Summarize long text into key points"
48
+ ),
49
+ SimpleAcp::Models::Capability.new(
50
+ name: "sentiment",
51
+ description: "Analyze the sentiment (positive/negative/neutral)"
52
+ ),
53
+ SimpleAcp::Models::Capability.new(
54
+ name: "keywords",
55
+ description: "Extract important keywords and phrases"
56
+ )
57
+ ],
58
+ domains: ["nlp", "text-analysis", "ai"],
59
+ tags: ["text", "analysis", "nlp", "summarization", "sentiment"],
60
+ links: [
61
+ SimpleAcp::Models::Link.new(
62
+ type: "documentation",
63
+ url: "https://example.com/docs/text-analyzer"
64
+ ),
65
+ SimpleAcp::Models::Link.new(
66
+ type: "source-code",
67
+ url: "https://github.com/example/text-analyzer"
68
+ ),
69
+ SimpleAcp::Models::Link.new(
70
+ type: "homepage",
71
+ url: "https://example.com/text-analyzer"
72
+ )
73
+ ],
74
+ dependencies: [
75
+ SimpleAcp::Models::Dependency.new(type: "agent", name: "tokenizer"),
76
+ SimpleAcp::Models::Dependency.new(type: "model", name: "gpt-4")
77
+ ],
78
+ recommended_models: ["gpt-4", "claude-3", "llama-2"]
79
+ )
80
+
81
+ server.agent("text-analyzer",
82
+ description: "Analyzes text for sentiment, keywords, and provides summaries",
83
+ input_content_types: ["text/plain", "text/markdown", "application/json"],
84
+ output_content_types: ["application/json", "text/plain"],
85
+ metadata: metadata
86
+ ) do |context|
87
+ text = context.input.first&.text_content || ""
88
+ operation = "full"
89
+
90
+ # Parse operation from JSON if provided
91
+ if context.input.first&.parts&.first&.content_type == "application/json"
92
+ begin
93
+ data = JSON.parse(context.input.first.parts.first.content)
94
+ text = data["text"] || text
95
+ operation = data["operation"] || "full"
96
+ rescue JSON::ParserError
97
+ # Use text as-is
98
+ end
99
+ end
100
+
101
+ # Simulate text analysis
102
+ words = text.split(/\s+/)
103
+ word_count = words.length
104
+
105
+ # Simple keyword extraction (just take unique words > 4 chars)
106
+ keywords = words.map(&:downcase).uniq.select { |w| w.length > 4 }.first(5)
107
+
108
+ # Simple sentiment (just check for positive/negative words)
109
+ positive_words = %w[good great excellent amazing wonderful happy love best fantastic]
110
+ negative_words = %w[bad terrible awful horrible sad hate worst disappointing]
111
+
112
+ positive_count = words.count { |w| positive_words.include?(w.downcase) }
113
+ negative_count = words.count { |w| negative_words.include?(w.downcase) }
114
+
115
+ sentiment = if positive_count > negative_count
116
+ "positive"
117
+ elsif negative_count > positive_count
118
+ "negative"
119
+ else
120
+ "neutral"
121
+ end
122
+
123
+ result = {
124
+ text_length: text.length,
125
+ word_count: word_count,
126
+ keywords: keywords,
127
+ sentiment: sentiment,
128
+ summary: words.first(20).join(" ") + (words.length > 20 ? "..." : "")
129
+ }
130
+
131
+ SimpleAcp::Models::Message.new(
132
+ role: "agent",
133
+ parts: [
134
+ SimpleAcp::Models::MessagePart.json(result),
135
+ SimpleAcp::Models::MessagePart.text(
136
+ "Analysis complete: #{word_count} words, sentiment: #{sentiment}"
137
+ )
138
+ ]
139
+ )
140
+ end
141
+
142
+ # Minimal agent for comparison
143
+ server.agent("simple-echo",
144
+ description: "A simple echo agent with minimal metadata"
145
+ ) do |context|
146
+ input = context.input.first&.text_content || ""
147
+ SimpleAcp::Models::Message.agent("Echo: #{input}")
148
+ end
149
+
150
+ # Agent with specific content types
151
+ server.agent("json-processor",
152
+ description: "Processes JSON input and returns JSON output",
153
+ input_content_types: ["application/json"],
154
+ output_content_types: ["application/json"],
155
+ metadata: SimpleAcp::Models::Metadata.new(
156
+ documentation: "A JSON processor that transforms input data",
157
+ domains: ["data-processing"],
158
+ tags: ["json", "transform"]
159
+ )
160
+ ) do |context|
161
+ begin
162
+ input_data = JSON.parse(context.input.first&.text_content || "{}")
163
+ output = {
164
+ received: input_data,
165
+ processed_at: Time.now.iso8601,
166
+ keys: input_data.keys
167
+ }
168
+ SimpleAcp::Models::Message.new(
169
+ role: "agent",
170
+ parts: [SimpleAcp::Models::MessagePart.json(output)]
171
+ )
172
+ rescue JSON::ParserError => e
173
+ SimpleAcp::Models::Message.new(
174
+ role: "agent",
175
+ parts: [SimpleAcp::Models::MessagePart.json({ error: e.message })]
176
+ )
177
+ end
178
+ end
179
+
180
+ puts "Starting Agent Metadata Demo Server..."
181
+ puts "Available agents:"
182
+ server.agents.each do |name, agent|
183
+ puts " - #{name}: #{agent.description}"
184
+ puts " Input types: #{agent.manifest.input_content_types.join(', ')}"
185
+ puts " Output types: #{agent.manifest.output_content_types.join(', ')}"
186
+ if agent.manifest.metadata
187
+ puts " Has metadata: author, capabilities, links, etc."
188
+ end
189
+ end
190
+ puts
191
+
192
+ server.run(port: 8000)