rubyrana 0.1.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 +12 -0
- data/README.md +3 -5
- data/docs/INDEX.md +16 -0
- data/docs/{USAGE.md → guides/USAGE.md} +7 -0
- data/docs/operations/DEV_WORKFLOW.md +16 -0
- data/docs/{RELEASE.md → operations/RELEASE.md} +1 -1
- data/docs/roadmap/TASKS.md +46 -0
- data/docs/roadmap/TODO.md +23 -0
- data/examples/README.md +9 -0
- data/lib/rubyrana/a2a/agent.rb +119 -0
- data/lib/rubyrana/a2a/client.rb +105 -0
- data/lib/rubyrana/a2a/converters.rb +91 -0
- data/lib/rubyrana/a2a/types.rb +27 -0
- data/lib/rubyrana/agent.rb +350 -43
- data/lib/rubyrana/config.rb +45 -1
- data/lib/rubyrana/errors.rb +2 -0
- data/lib/rubyrana/event_loop/events.rb +15 -0
- data/lib/rubyrana/event_loop/runner.rb +117 -0
- data/lib/rubyrana/hooks/base.rb +12 -0
- data/lib/rubyrana/hooks/events.rb +16 -0
- data/lib/rubyrana/hooks/provider.rb +11 -0
- data/lib/rubyrana/hooks/registry.rb +77 -0
- data/lib/rubyrana/limits/rate_limiter.rb +35 -0
- data/lib/rubyrana/limits/semaphore.rb +30 -0
- data/lib/rubyrana/memory/strategy.rb +37 -0
- data/lib/rubyrana/multi_agent.rb +43 -0
- data/lib/rubyrana/multiagent/router.rb +33 -0
- data/lib/rubyrana/providers/anthropic.rb +88 -47
- data/lib/rubyrana/providers/base.rb +51 -0
- data/lib/rubyrana/retry/circuit_breaker.rb +73 -0
- data/lib/rubyrana/retry/policy.rb +39 -0
- data/lib/rubyrana/session/context.rb +14 -0
- data/lib/rubyrana/session/file_repository.rb +157 -0
- data/lib/rubyrana/session/redis_repository.rb +157 -0
- data/lib/rubyrana/session/repository.rb +180 -0
- data/lib/rubyrana/session/types.rb +9 -0
- data/lib/rubyrana/telemetry/exporter.rb +11 -0
- data/lib/rubyrana/telemetry/jsonl_exporter.rb +22 -0
- data/lib/rubyrana/telemetry/metrics.rb +64 -0
- data/lib/rubyrana/telemetry/tracer.rb +41 -0
- data/lib/rubyrana/tool.rb +14 -0
- data/lib/rubyrana/tools/structured_output/schema.rb +17 -0
- data/lib/rubyrana/tools/structured_output/tool.rb +40 -0
- data/lib/rubyrana/types/agent_result.rb +17 -0
- data/lib/rubyrana/types/interrupt.rb +7 -0
- data/lib/rubyrana/types/message.rb +7 -0
- data/lib/rubyrana/types/tool_result.rb +7 -0
- data/lib/rubyrana/types/tool_use.rb +7 -0
- data/lib/rubyrana/version.rb +1 -1
- data/lib/rubyrana.rb +34 -0
- metadata +66 -18
- /data/docs/{CHECKLIST.md → operations/CHECKLIST.md} +0 -0
- /data/{REPORT.md → docs/reports/REPORT.md} +0 -0
- /data/examples/{mcp.rb → advanced/mcp.rb} +0 -0
- /data/examples/{tools_loader.rb → advanced/tools_loader.rb} +0 -0
- /data/examples/{quick_start.rb → basic/quick_start.rb} +0 -0
- /data/examples/{streaming.rb → basic/streaming.rb} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 34608dd47bf0508e17e5f98c2234ea030a853dadd0dfbb22e14770a179bcd09e
|
|
4
|
+
data.tar.gz: 32e051b98dcea4c2246eff0c77b1f5d8b0cf5c4f7cdb2c409cead771ade22db6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 513bf981ad9bcf68f9891c94a81c3298c30e7b2fc25936c33789e9d10fa6e951afa883f740ccedc3e016749a43d327be4b8279f626d63e1c0ea026e4419fc9ca
|
|
7
|
+
data.tar.gz: 025dc718c6125ef006ad486bdb47f533e6372694ea664c9cd1dc3697773c02bfaf6837699c901e335c5d907c6e552774fc134cd5e7efa6b6d920f39d36d860e7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
- Add structured output support with retries and event loop integration
|
|
5
|
+
- Expand hooks taxonomy and provider registration
|
|
6
|
+
- Introduce session repository layer with in-memory, file, and Redis adapters
|
|
7
|
+
- Add A2A agent implementation and types
|
|
8
|
+
- Add event loop events, interrupts, and richer agent results
|
|
9
|
+
- Add multi-agent graph execution helper
|
|
10
|
+
- Reorganize docs/examples and move build artifacts/vendor snapshots
|
|
11
|
+
|
|
12
|
+
## 0.1.1
|
|
13
|
+
- Update gemspec homepage
|
|
14
|
+
|
|
3
15
|
## 0.1.0
|
|
4
16
|
- Initial MVP scaffolding
|
|
5
17
|
- Agent loop with tool calling
|
data/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
<div align="center">
|
|
10
10
|
<img alt="Gem version" src="https://img.shields.io/gem/v/rubyrana" />
|
|
11
|
-
<img alt="Ruby" src="https://img.shields.io/badge/ruby-%3E%3D%203.
|
|
11
|
+
<img alt="Ruby" src="https://img.shields.io/badge/ruby-%3E%3D%203.4-brightgreen" />
|
|
12
12
|
<img alt="License" src="https://img.shields.io/badge/license-Apache%202.0-blue" />
|
|
13
13
|
</div>
|
|
14
14
|
|
|
@@ -165,10 +165,8 @@ puts agent.call("Explain agentic workflows in simple terms")
|
|
|
165
165
|
|
|
166
166
|
## Documentation
|
|
167
167
|
|
|
168
|
-
-
|
|
169
|
-
-
|
|
170
|
-
- Tools & MCP
|
|
171
|
-
- Production Deployment
|
|
168
|
+
- [docs/INDEX.md](docs/INDEX.md)
|
|
169
|
+
- [examples/README.md](examples/README.md)
|
|
172
170
|
|
|
173
171
|
## Contributing
|
|
174
172
|
|
data/docs/INDEX.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Documentation Index
|
|
2
|
+
|
|
3
|
+
## Guides
|
|
4
|
+
- [docs/guides/USAGE.md](docs/guides/USAGE.md)
|
|
5
|
+
|
|
6
|
+
## Operations
|
|
7
|
+
- [docs/operations/DEV_WORKFLOW.md](docs/operations/DEV_WORKFLOW.md)
|
|
8
|
+
- [docs/operations/RELEASE.md](docs/operations/RELEASE.md)
|
|
9
|
+
- [docs/operations/CHECKLIST.md](docs/operations/CHECKLIST.md)
|
|
10
|
+
|
|
11
|
+
## Roadmap
|
|
12
|
+
- [docs/roadmap/TASKS.md](docs/roadmap/TASKS.md)
|
|
13
|
+
- [docs/roadmap/TODO.md](docs/roadmap/TODO.md)
|
|
14
|
+
|
|
15
|
+
## Reports
|
|
16
|
+
- [docs/reports/REPORT.md](docs/reports/REPORT.md)
|
|
@@ -46,6 +46,13 @@ Load tools from a directory:
|
|
|
46
46
|
|
|
47
47
|
- agent = Rubyrana::Agent.new(load_tools_from: "./tools")
|
|
48
48
|
|
|
49
|
+
## Memory Strategies
|
|
50
|
+
|
|
51
|
+
Use a rolling window to cap context size:
|
|
52
|
+
|
|
53
|
+
- strategy = Rubyrana::Memory::RollingWindow.new(max_messages: 20)
|
|
54
|
+
- agent = Rubyrana::Agent.new(memory_strategy: strategy)
|
|
55
|
+
|
|
49
56
|
## MCP (Experimental)
|
|
50
57
|
|
|
51
58
|
Use the MCP client to load tools:
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Dev Workflow
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
- `bundle install`
|
|
5
|
+
|
|
6
|
+
## Run tests (fast feedback)
|
|
7
|
+
- `scripts/run_tests.sh`
|
|
8
|
+
|
|
9
|
+
## Run an example
|
|
10
|
+
- `scripts/run_example.sh examples/quick_start.rb`
|
|
11
|
+
|
|
12
|
+
## Suggested loop
|
|
13
|
+
1) Implement a small change
|
|
14
|
+
2) Run tests
|
|
15
|
+
3) Run a relevant example
|
|
16
|
+
4) Commit
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Rubyrana Full‑Fledge Roadmap
|
|
2
|
+
|
|
3
|
+
## 1) Anthropic Depth
|
|
4
|
+
- Add system prompts support
|
|
5
|
+
- Add tool_choice controls
|
|
6
|
+
- Add thinking/analysis options
|
|
7
|
+
- Add response format tuning
|
|
8
|
+
- Map provider errors to friendly categories
|
|
9
|
+
- Expand provider tests
|
|
10
|
+
|
|
11
|
+
## 2) Tooling Parity
|
|
12
|
+
- Add tool schema validation
|
|
13
|
+
- Support structured tool results
|
|
14
|
+
- Add tool choice policies
|
|
15
|
+
- Improve tool debugging and tracing
|
|
16
|
+
- Optional tool hot‑reload
|
|
17
|
+
|
|
18
|
+
## 3) Memory & Context
|
|
19
|
+
- Context window trimming
|
|
20
|
+
- Summarization memory
|
|
21
|
+
- Token budgeting
|
|
22
|
+
- Memory strategy options (rolling, summary)
|
|
23
|
+
|
|
24
|
+
## 4) Streaming Robustness
|
|
25
|
+
- Partial chunk handling
|
|
26
|
+
- Tool‑use streaming
|
|
27
|
+
- Usage metadata during streaming
|
|
28
|
+
- Streaming edge‑case tests
|
|
29
|
+
|
|
30
|
+
## 5) Observability
|
|
31
|
+
- Request/trace IDs
|
|
32
|
+
- Metrics hooks
|
|
33
|
+
- Structured logs
|
|
34
|
+
- Provider/tool timing
|
|
35
|
+
|
|
36
|
+
## 6) Tool Ecosystem
|
|
37
|
+
- Split built‑in tools into packs
|
|
38
|
+
- MCP tool discovery examples
|
|
39
|
+
- Tool pack publishing guide
|
|
40
|
+
|
|
41
|
+
## 7) Docs & Examples
|
|
42
|
+
- Multi‑agent routing example
|
|
43
|
+
- Safety filter example
|
|
44
|
+
- Persistence example
|
|
45
|
+
- MCP web search example
|
|
46
|
+
- Deployment guide
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Rubyrana Roadmap (Post-MVP)
|
|
2
|
+
|
|
3
|
+
## Core Agent Capabilities
|
|
4
|
+
- [x] Conversation memory and history management
|
|
5
|
+
- [x] Structured tool-calling semantics per provider
|
|
6
|
+
- [x] Native streaming for Anthropic
|
|
7
|
+
- [x] Token usage + cost tracking
|
|
8
|
+
|
|
9
|
+
## Developer Experience
|
|
10
|
+
- [x] Rich examples (tools, MCP, streaming, providers)
|
|
11
|
+
- [x] Error messages and retries tuned per provider
|
|
12
|
+
- [x] Logging hooks and debug mode
|
|
13
|
+
|
|
14
|
+
## Features Beyond MVP
|
|
15
|
+
- [x] Multi-agent orchestration
|
|
16
|
+
- [x] Task routing / tool selection policies
|
|
17
|
+
- [x] Configurable safety filters
|
|
18
|
+
- [x] Persistence adapters (file/redis)
|
|
19
|
+
|
|
20
|
+
## Production Readiness
|
|
21
|
+
- [x] Test coverage expansion
|
|
22
|
+
- [x] CI enhancements (lint)
|
|
23
|
+
- [x] Release automation
|
data/examples/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
## Basic
|
|
4
|
+
- [examples/basic/quick_start.rb](examples/basic/quick_start.rb)
|
|
5
|
+
- [examples/basic/streaming.rb](examples/basic/streaming.rb)
|
|
6
|
+
|
|
7
|
+
## Advanced
|
|
8
|
+
- [examples/advanced/mcp.rb](examples/advanced/mcp.rb)
|
|
9
|
+
- [examples/advanced/tools_loader.rb](examples/advanced/tools_loader.rb)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubyrana
|
|
4
|
+
module A2A
|
|
5
|
+
class Agent
|
|
6
|
+
DEFAULT_TIMEOUT = 300
|
|
7
|
+
|
|
8
|
+
attr_reader :endpoint, :timeout, :name, :description
|
|
9
|
+
|
|
10
|
+
def initialize(endpoint:, name: nil, description: nil, timeout: DEFAULT_TIMEOUT, a2a_client_factory: nil, card_resolver: nil)
|
|
11
|
+
@endpoint = endpoint
|
|
12
|
+
@name = name
|
|
13
|
+
@description = description
|
|
14
|
+
@timeout = timeout
|
|
15
|
+
@agent_card = nil
|
|
16
|
+
@a2a_client_factory = a2a_client_factory
|
|
17
|
+
@card_resolver = card_resolver
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call(prompt = nil, **_kwargs)
|
|
21
|
+
invoke(prompt)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def invoke(prompt = nil, **_kwargs)
|
|
25
|
+
result = nil
|
|
26
|
+
stream(prompt) do |event|
|
|
27
|
+
result = event[:result] if event[:result]
|
|
28
|
+
end
|
|
29
|
+
raise RuntimeError, "No response received from A2A agent" unless result
|
|
30
|
+
|
|
31
|
+
result
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def stream(prompt = nil, **_kwargs)
|
|
35
|
+
raise ArgumentError, "prompt is required" if prompt.nil?
|
|
36
|
+
|
|
37
|
+
last_event = nil
|
|
38
|
+
last_complete_event = nil
|
|
39
|
+
enumerator = _send_message(prompt)
|
|
40
|
+
|
|
41
|
+
if block_given?
|
|
42
|
+
enumerator.each do |event|
|
|
43
|
+
last_event = event
|
|
44
|
+
last_complete_event = event if complete_event?(event)
|
|
45
|
+
yield({ type: "a2a_stream", event: event })
|
|
46
|
+
end
|
|
47
|
+
final_event = last_complete_event || last_event
|
|
48
|
+
if final_event
|
|
49
|
+
result = Rubyrana::A2A::Converters.convert_response_to_agent_result(final_event)
|
|
50
|
+
yield({ result: result })
|
|
51
|
+
end
|
|
52
|
+
return
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
Enumerator.new do |yielder|
|
|
56
|
+
enumerator.each do |event|
|
|
57
|
+
last_event = event
|
|
58
|
+
last_complete_event = event if complete_event?(event)
|
|
59
|
+
yielder << { type: "a2a_stream", event: event }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
final_event = last_complete_event || last_event
|
|
63
|
+
if final_event
|
|
64
|
+
result = Rubyrana::A2A::Converters.convert_response_to_agent_result(final_event)
|
|
65
|
+
yielder << { result: result }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def get_agent_card
|
|
71
|
+
return @agent_card if @agent_card
|
|
72
|
+
|
|
73
|
+
resolver = @card_resolver || Rubyrana::A2A::CardResolver.new(base_url: endpoint)
|
|
74
|
+
@agent_card = resolver.get_agent_card
|
|
75
|
+
|
|
76
|
+
@name ||= @agent_card.name
|
|
77
|
+
@description ||= @agent_card.description
|
|
78
|
+
|
|
79
|
+
@agent_card
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def _send_message(prompt)
|
|
85
|
+
message = Rubyrana::A2A::Converters.convert_input_to_message(prompt)
|
|
86
|
+
client = a2a_client
|
|
87
|
+
client.send_message(message)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def a2a_client
|
|
91
|
+
agent_card = get_agent_card
|
|
92
|
+
factory = @a2a_client_factory || Rubyrana::A2A::ClientFactory.new
|
|
93
|
+
factory.create(agent_card)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def complete_event?(event)
|
|
97
|
+
return true if event.is_a?(Rubyrana::A2A::Message)
|
|
98
|
+
|
|
99
|
+
if event.is_a?(Array) && event.length == 2
|
|
100
|
+
task, update_event = event
|
|
101
|
+
return true if update_event.nil?
|
|
102
|
+
|
|
103
|
+
if update_event.is_a?(Rubyrana::A2A::TaskArtifactUpdateEvent)
|
|
104
|
+
return update_event.last_chunk unless update_event.last_chunk.nil?
|
|
105
|
+
return false
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
if update_event.is_a?(Rubyrana::A2A::TaskStatusUpdateEvent)
|
|
109
|
+
return update_event.status.state == "completed" if update_event.status&.state
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
return false
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
false
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Rubyrana
|
|
7
|
+
module A2A
|
|
8
|
+
class CardResolver
|
|
9
|
+
def initialize(base_url:, http_client: nil)
|
|
10
|
+
@base_url = base_url
|
|
11
|
+
@http_client = http_client
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def get_agent_card
|
|
15
|
+
uri = URI.join(@base_url, "/.well-known/agent-card")
|
|
16
|
+
response = http_client.get(uri)
|
|
17
|
+
raise Rubyrana::ProviderError, "A2A agent card request failed (status #{response.code})" unless response.is_a?(Net::HTTPSuccess)
|
|
18
|
+
|
|
19
|
+
body = JSON.parse(response.body)
|
|
20
|
+
Rubyrana::A2A::AgentCard.new(
|
|
21
|
+
name: body["name"],
|
|
22
|
+
description: body["description"],
|
|
23
|
+
url: body["url"],
|
|
24
|
+
version: body["version"],
|
|
25
|
+
capabilities: body["capabilities"],
|
|
26
|
+
default_input_modes: body["default_input_modes"] || [],
|
|
27
|
+
default_output_modes: body["default_output_modes"] || [],
|
|
28
|
+
skills: body["skills"] || []
|
|
29
|
+
)
|
|
30
|
+
rescue JSON::ParserError => e
|
|
31
|
+
raise Rubyrana::ProviderError, e.message
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def http_client
|
|
37
|
+
@http_client ||= Net::HTTP
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class ClientFactory
|
|
42
|
+
def initialize(http_client: nil, streaming: true)
|
|
43
|
+
@http_client = http_client
|
|
44
|
+
@streaming = streaming
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def create(agent_card)
|
|
48
|
+
Client.new(agent_card.url, http_client: @http_client, streaming: @streaming)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class Client
|
|
53
|
+
def initialize(base_url, http_client: nil, streaming: true)
|
|
54
|
+
@base_url = base_url
|
|
55
|
+
@http_client = http_client || Net::HTTP
|
|
56
|
+
@streaming = streaming
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def send_message(message)
|
|
60
|
+
response = post_message(message)
|
|
61
|
+
parsed = parse_message_response(response)
|
|
62
|
+
|
|
63
|
+
Enumerator.new do |yielder|
|
|
64
|
+
yielder << parsed
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def post_message(message)
|
|
71
|
+
uri = URI.join(@base_url, "/messages")
|
|
72
|
+
request = Net::HTTP::Post.new(uri)
|
|
73
|
+
request["Content-Type"] = "application/json"
|
|
74
|
+
request.body = JSON.dump({ message: serialize_message(message) })
|
|
75
|
+
|
|
76
|
+
@http_client.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
77
|
+
http.request(request)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def parse_message_response(response)
|
|
82
|
+
raise Rubyrana::ProviderError, "A2A message request failed (status #{response.code})" unless response.is_a?(Net::HTTPSuccess)
|
|
83
|
+
|
|
84
|
+
body = JSON.parse(response.body)
|
|
85
|
+
message = body["message"] || body
|
|
86
|
+
parts = Array(message["parts"]).map { |part| Rubyrana::A2A::Part.new(kind: part["kind"], text: part["text"]) }
|
|
87
|
+
Rubyrana::A2A::Message.new(
|
|
88
|
+
message_id: message["message_id"] || SecureRandom.uuid,
|
|
89
|
+
role: message["role"] || "agent",
|
|
90
|
+
parts: parts
|
|
91
|
+
)
|
|
92
|
+
rescue JSON::ParserError => e
|
|
93
|
+
raise Rubyrana::ProviderError, e.message
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def serialize_message(message)
|
|
97
|
+
{
|
|
98
|
+
message_id: message.message_id,
|
|
99
|
+
role: message.role,
|
|
100
|
+
parts: message.parts.map { |part| { kind: part.kind, text: part.text } }
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubyrana
|
|
4
|
+
module A2A
|
|
5
|
+
module Converters
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def convert_input_to_message(prompt)
|
|
9
|
+
return prompt if prompt.is_a?(Rubyrana::A2A::Message)
|
|
10
|
+
|
|
11
|
+
if prompt.is_a?(Hash)
|
|
12
|
+
role = prompt[:role] || prompt["role"] || "user"
|
|
13
|
+
content = prompt[:content] || prompt["content"] || ""
|
|
14
|
+
return build_message(role, content)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
if prompt.is_a?(Array)
|
|
18
|
+
return build_message("user", prompt)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
build_message("user", prompt.to_s)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def convert_response_to_agent_result(event)
|
|
25
|
+
message = extract_message(event)
|
|
26
|
+
text = extract_text_from_message(message)
|
|
27
|
+
Rubyrana::Types::AgentResult.new(
|
|
28
|
+
text: text,
|
|
29
|
+
message: message,
|
|
30
|
+
stop_reason: "end_turn",
|
|
31
|
+
tool_results: [],
|
|
32
|
+
tool_uses: [],
|
|
33
|
+
interrupts: []
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def build_message(role, content)
|
|
38
|
+
parts = normalize_parts(content)
|
|
39
|
+
Rubyrana::A2A::Message.new(
|
|
40
|
+
message_id: SecureRandom.uuid,
|
|
41
|
+
role: role,
|
|
42
|
+
parts: parts
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def normalize_parts(content)
|
|
47
|
+
return content.map { |item| to_part(item) } if content.is_a?(Array)
|
|
48
|
+
|
|
49
|
+
[to_part(content)]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def to_part(item)
|
|
53
|
+
return item if item.is_a?(Rubyrana::A2A::Part) || item.is_a?(Rubyrana::A2A::TextPart)
|
|
54
|
+
|
|
55
|
+
if item.is_a?(Hash)
|
|
56
|
+
kind = item[:kind] || item["kind"] || "text"
|
|
57
|
+
text = item[:text] || item["text"] || item[:content] || item["content"] || item.to_s
|
|
58
|
+
return Rubyrana::A2A::Part.new(kind: kind, text: text)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
Rubyrana::A2A::TextPart.new(kind: "text", text: item.to_s)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def extract_message(event)
|
|
65
|
+
return message_to_hash(event) if event.is_a?(Rubyrana::A2A::Message)
|
|
66
|
+
|
|
67
|
+
if event.is_a?(Array) && event.length == 2
|
|
68
|
+
task = event[0]
|
|
69
|
+
return message_to_hash(task.message) if task.respond_to?(:message) && task.message
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
if event.is_a?(Hash)
|
|
73
|
+
return event if event.key?(:role) || event.key?("role")
|
|
74
|
+
return event[:message] || event["message"] if event[:message] || event["message"]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
{ role: "assistant", content: [{ text: event.to_s }] }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def message_to_hash(message)
|
|
81
|
+
content = message.parts.to_a.map { |part| { text: part.text } }
|
|
82
|
+
{ role: message.role, content: content }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def extract_text_from_message(message)
|
|
86
|
+
content = message[:content] || message["content"] || []
|
|
87
|
+
content.map { |item| item[:text] || item["text"] }.compact.join
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubyrana
|
|
4
|
+
module A2A
|
|
5
|
+
AgentCard = Struct.new(
|
|
6
|
+
:name,
|
|
7
|
+
:description,
|
|
8
|
+
:url,
|
|
9
|
+
:version,
|
|
10
|
+
:capabilities,
|
|
11
|
+
:default_input_modes,
|
|
12
|
+
:default_output_modes,
|
|
13
|
+
:skills,
|
|
14
|
+
keyword_init: true
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
TextPart = Struct.new(:kind, :text, keyword_init: true)
|
|
18
|
+
Part = Struct.new(:kind, :text, keyword_init: true)
|
|
19
|
+
|
|
20
|
+
Message = Struct.new(:message_id, :role, :parts, keyword_init: true)
|
|
21
|
+
|
|
22
|
+
Task = Struct.new(:id, :status, :message, keyword_init: true)
|
|
23
|
+
TaskStatus = Struct.new(:state, keyword_init: true)
|
|
24
|
+
TaskStatusUpdateEvent = Struct.new(:status, keyword_init: true)
|
|
25
|
+
TaskArtifactUpdateEvent = Struct.new(:last_chunk, keyword_init: true)
|
|
26
|
+
end
|
|
27
|
+
end
|