prompt_objects 0.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.
- checksums.yaml +7 -0
- data/CLAUDE.md +108 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +231 -0
- data/IMPLEMENTATION_PLAN.md +1073 -0
- data/LICENSE +21 -0
- data/README.md +73 -0
- data/Rakefile +27 -0
- data/design-doc-v2.md +1232 -0
- data/exe/prompt_objects +572 -0
- data/exe/prompt_objects_mcp +34 -0
- data/frontend/.gitignore +3 -0
- data/frontend/index.html +13 -0
- data/frontend/package-lock.json +4417 -0
- data/frontend/package.json +32 -0
- data/frontend/postcss.config.js +6 -0
- data/frontend/src/App.tsx +95 -0
- data/frontend/src/components/CapabilitiesPanel.tsx +44 -0
- data/frontend/src/components/ChatPanel.tsx +251 -0
- data/frontend/src/components/Dashboard.tsx +83 -0
- data/frontend/src/components/Header.tsx +141 -0
- data/frontend/src/components/MarkdownMessage.tsx +153 -0
- data/frontend/src/components/MessageBus.tsx +55 -0
- data/frontend/src/components/ModelSelector.tsx +112 -0
- data/frontend/src/components/NotificationPanel.tsx +134 -0
- data/frontend/src/components/POCard.tsx +56 -0
- data/frontend/src/components/PODetail.tsx +117 -0
- data/frontend/src/components/PromptPanel.tsx +51 -0
- data/frontend/src/components/SessionsPanel.tsx +174 -0
- data/frontend/src/components/ThreadsSidebar.tsx +119 -0
- data/frontend/src/components/index.ts +11 -0
- data/frontend/src/hooks/useWebSocket.ts +363 -0
- data/frontend/src/index.css +37 -0
- data/frontend/src/main.tsx +10 -0
- data/frontend/src/store/index.ts +246 -0
- data/frontend/src/types/index.ts +146 -0
- data/frontend/tailwind.config.js +25 -0
- data/frontend/tsconfig.json +30 -0
- data/frontend/vite.config.ts +29 -0
- data/lib/prompt_objects/capability.rb +46 -0
- data/lib/prompt_objects/cli.rb +431 -0
- data/lib/prompt_objects/connectors/base.rb +73 -0
- data/lib/prompt_objects/connectors/mcp.rb +524 -0
- data/lib/prompt_objects/environment/exporter.rb +83 -0
- data/lib/prompt_objects/environment/git.rb +118 -0
- data/lib/prompt_objects/environment/importer.rb +159 -0
- data/lib/prompt_objects/environment/manager.rb +401 -0
- data/lib/prompt_objects/environment/manifest.rb +218 -0
- data/lib/prompt_objects/environment.rb +283 -0
- data/lib/prompt_objects/human_queue.rb +144 -0
- data/lib/prompt_objects/llm/anthropic_adapter.rb +137 -0
- data/lib/prompt_objects/llm/factory.rb +84 -0
- data/lib/prompt_objects/llm/gemini_adapter.rb +209 -0
- data/lib/prompt_objects/llm/openai_adapter.rb +104 -0
- data/lib/prompt_objects/llm/response.rb +61 -0
- data/lib/prompt_objects/loader.rb +32 -0
- data/lib/prompt_objects/mcp/server.rb +167 -0
- data/lib/prompt_objects/mcp/tools/get_conversation.rb +60 -0
- data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +54 -0
- data/lib/prompt_objects/mcp/tools/inspect_po.rb +73 -0
- data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +37 -0
- data/lib/prompt_objects/mcp/tools/respond_to_request.rb +68 -0
- data/lib/prompt_objects/mcp/tools/send_message.rb +71 -0
- data/lib/prompt_objects/message_bus.rb +97 -0
- data/lib/prompt_objects/primitive.rb +13 -0
- data/lib/prompt_objects/primitives/http_get.rb +72 -0
- data/lib/prompt_objects/primitives/list_files.rb +95 -0
- data/lib/prompt_objects/primitives/read_file.rb +81 -0
- data/lib/prompt_objects/primitives/write_file.rb +73 -0
- data/lib/prompt_objects/prompt_object.rb +415 -0
- data/lib/prompt_objects/registry.rb +88 -0
- data/lib/prompt_objects/server/api/routes.rb +297 -0
- data/lib/prompt_objects/server/app.rb +174 -0
- data/lib/prompt_objects/server/file_watcher.rb +113 -0
- data/lib/prompt_objects/server/public/assets/index-2acS2FYZ.js +77 -0
- data/lib/prompt_objects/server/public/assets/index-DXU5uRXQ.css +1 -0
- data/lib/prompt_objects/server/public/index.html +14 -0
- data/lib/prompt_objects/server/websocket_handler.rb +619 -0
- data/lib/prompt_objects/server.rb +166 -0
- data/lib/prompt_objects/session/store.rb +826 -0
- data/lib/prompt_objects/universal/add_capability.rb +74 -0
- data/lib/prompt_objects/universal/add_primitive.rb +113 -0
- data/lib/prompt_objects/universal/ask_human.rb +109 -0
- data/lib/prompt_objects/universal/create_capability.rb +219 -0
- data/lib/prompt_objects/universal/create_primitive.rb +170 -0
- data/lib/prompt_objects/universal/list_capabilities.rb +55 -0
- data/lib/prompt_objects/universal/list_primitives.rb +145 -0
- data/lib/prompt_objects/universal/modify_primitive.rb +180 -0
- data/lib/prompt_objects/universal/request_primitive.rb +287 -0
- data/lib/prompt_objects/universal/think.rb +41 -0
- data/lib/prompt_objects/universal/verify_primitive.rb +173 -0
- data/lib/prompt_objects.rb +62 -0
- data/objects/coordinator.md +48 -0
- data/objects/greeter.md +30 -0
- data/objects/reader.md +33 -0
- data/prompt_objects.gemspec +50 -0
- data/templates/basic/.gitignore +2 -0
- data/templates/basic/manifest.yml +7 -0
- data/templates/basic/objects/basic.md +32 -0
- data/templates/developer/.gitignore +5 -0
- data/templates/developer/manifest.yml +17 -0
- data/templates/developer/objects/code_reviewer.md +33 -0
- data/templates/developer/objects/coordinator.md +39 -0
- data/templates/developer/objects/debugger.md +35 -0
- data/templates/empty/.gitignore +5 -0
- data/templates/empty/manifest.yml +14 -0
- data/templates/empty/objects/.gitkeep +0 -0
- data/templates/empty/objects/assistant.md +41 -0
- data/templates/minimal/.gitignore +5 -0
- data/templates/minimal/manifest.yml +7 -0
- data/templates/minimal/objects/assistant.md +41 -0
- data/templates/writer/.gitignore +5 -0
- data/templates/writer/manifest.yml +17 -0
- data/templates/writer/objects/coordinator.md +33 -0
- data/templates/writer/objects/editor.md +33 -0
- data/templates/writer/objects/researcher.md +34 -0
- metadata +343 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptObjects
|
|
4
|
+
# A Prompt Object is a capability backed by an LLM.
|
|
5
|
+
# It interprets messages semantically using its markdown "soul" as the system prompt.
|
|
6
|
+
class PromptObject < Capability
|
|
7
|
+
attr_reader :config, :body, :history, :session_id, :path
|
|
8
|
+
|
|
9
|
+
# @param config [Hash] Parsed frontmatter (name, description, capabilities)
|
|
10
|
+
# @param body [String] Markdown body (the "soul" - becomes system prompt)
|
|
11
|
+
# @param env [Environment] Reference to the environment
|
|
12
|
+
# @param llm [LLM::OpenAIAdapter] LLM adapter for making calls
|
|
13
|
+
# @param path [String, nil] Path to the source .md file (for persistence)
|
|
14
|
+
def initialize(config:, body:, env:, llm:, path: nil)
|
|
15
|
+
super()
|
|
16
|
+
@config = config
|
|
17
|
+
@body = body
|
|
18
|
+
@env = env
|
|
19
|
+
@llm = llm
|
|
20
|
+
@path = path
|
|
21
|
+
@history = []
|
|
22
|
+
@session_id = nil
|
|
23
|
+
|
|
24
|
+
# Load existing session if session store is available
|
|
25
|
+
load_or_create_session if session_store
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Get the session store from the environment.
|
|
29
|
+
# @return [Session::Store, nil]
|
|
30
|
+
def session_store
|
|
31
|
+
@env.session_store
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def name
|
|
35
|
+
@config["name"] || "unnamed"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def description
|
|
39
|
+
@config["description"] || "A prompt object"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Prompt objects accept natural language messages.
|
|
43
|
+
def parameters
|
|
44
|
+
{
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {
|
|
47
|
+
message: {
|
|
48
|
+
type: "string",
|
|
49
|
+
description: "Natural language message to send"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
required: ["message"]
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Handle an incoming message by running the LLM conversation loop.
|
|
57
|
+
# @param message [String, Hash] The incoming message
|
|
58
|
+
# @param context [Hash] Execution context
|
|
59
|
+
# @return [String] The response
|
|
60
|
+
def receive(message, context:)
|
|
61
|
+
# Normalize message to string
|
|
62
|
+
content = normalize_message(message)
|
|
63
|
+
|
|
64
|
+
# Track who sent this message - another PO or a human?
|
|
65
|
+
sender = context.current_capability
|
|
66
|
+
from = (sender && sender != name) ? sender : "human"
|
|
67
|
+
|
|
68
|
+
user_msg = { role: :user, content: content, from: from }
|
|
69
|
+
@history << user_msg
|
|
70
|
+
persist_message(user_msg)
|
|
71
|
+
@state = :working
|
|
72
|
+
|
|
73
|
+
# Conversation loop - keep going until LLM responds without tool calls
|
|
74
|
+
loop do
|
|
75
|
+
response = @llm.chat(
|
|
76
|
+
system: build_system_prompt,
|
|
77
|
+
messages: @history,
|
|
78
|
+
tools: available_tool_descriptors
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if response.tool_calls?
|
|
82
|
+
# Execute tools and continue the loop
|
|
83
|
+
results = execute_tool_calls(response.tool_calls, context)
|
|
84
|
+
assistant_msg = {
|
|
85
|
+
role: :assistant,
|
|
86
|
+
# Don't include content when there are tool calls - force LLM to
|
|
87
|
+
# wait for tool results before generating a response. This prevents
|
|
88
|
+
# the model from "hedging" by generating both a response AND a tool call.
|
|
89
|
+
content: nil,
|
|
90
|
+
tool_calls: response.tool_calls
|
|
91
|
+
}
|
|
92
|
+
@history << assistant_msg
|
|
93
|
+
persist_message(assistant_msg)
|
|
94
|
+
|
|
95
|
+
tool_msg = { role: :tool, results: results }
|
|
96
|
+
@history << tool_msg
|
|
97
|
+
persist_message(tool_msg)
|
|
98
|
+
else
|
|
99
|
+
# No tool calls - we have our final response
|
|
100
|
+
assistant_msg = { role: :assistant, content: response.content }
|
|
101
|
+
@history << assistant_msg
|
|
102
|
+
persist_message(assistant_msg)
|
|
103
|
+
@state = :idle
|
|
104
|
+
return response.content
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# --- Session Management ---
|
|
110
|
+
|
|
111
|
+
# List all sessions for this PO.
|
|
112
|
+
# @return [Array<Hash>] Session data
|
|
113
|
+
def list_sessions
|
|
114
|
+
return [] unless session_store
|
|
115
|
+
|
|
116
|
+
session_store.list_sessions(po_name: name)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Switch to a different session.
|
|
120
|
+
# @param session_id [String] Session ID to switch to
|
|
121
|
+
# @return [Boolean] True if switch was successful
|
|
122
|
+
def switch_session(session_id)
|
|
123
|
+
return false unless session_store
|
|
124
|
+
|
|
125
|
+
session = session_store.get_session(session_id)
|
|
126
|
+
return false unless session && session[:po_name] == name
|
|
127
|
+
|
|
128
|
+
@session_id = session_id
|
|
129
|
+
reload_history_from_session
|
|
130
|
+
true
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Create a new session and switch to it.
|
|
134
|
+
# @param name [String, nil] Optional session name
|
|
135
|
+
# @return [String] New session ID
|
|
136
|
+
def new_session(name: nil)
|
|
137
|
+
return nil unless session_store
|
|
138
|
+
|
|
139
|
+
@session_id = session_store.create_session(po_name: self.name, name: name)
|
|
140
|
+
@history = []
|
|
141
|
+
@session_id
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Clear the current session's history.
|
|
145
|
+
def clear_history
|
|
146
|
+
@history = []
|
|
147
|
+
session_store&.clear_messages(@session_id) if @session_id
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# --- File Persistence ---
|
|
151
|
+
|
|
152
|
+
# Save the current config and body back to the source file.
|
|
153
|
+
# This persists any runtime changes (like added capabilities) to disk.
|
|
154
|
+
# @return [Boolean] True if saved successfully, false if no path or error
|
|
155
|
+
def save
|
|
156
|
+
return false unless @path
|
|
157
|
+
|
|
158
|
+
# Build YAML frontmatter with proper formatting
|
|
159
|
+
yaml_content = @config.to_yaml
|
|
160
|
+
|
|
161
|
+
# Combine frontmatter and body
|
|
162
|
+
content = "#{yaml_content}---\n\n#{@body}\n"
|
|
163
|
+
|
|
164
|
+
File.write(@path, content, encoding: "UTF-8")
|
|
165
|
+
true
|
|
166
|
+
rescue => e
|
|
167
|
+
puts "Error saving PO #{name} to #{@path}: #{e.message}" if ENV["DEBUG"]
|
|
168
|
+
false
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# --- Thread/Delegation Support ---
|
|
172
|
+
|
|
173
|
+
# Create a delegation thread for handling a call from another PO.
|
|
174
|
+
# @param parent_po [String] Name of the PO that initiated the call
|
|
175
|
+
# @param parent_session_id [String] Session ID in the parent PO
|
|
176
|
+
# @param parent_message_id [Integer, nil] Message ID that triggered the delegation
|
|
177
|
+
# @return [String, nil] New thread ID or nil if no session store
|
|
178
|
+
def create_delegation_thread(parent_po:, parent_session_id:, parent_message_id: nil)
|
|
179
|
+
return nil unless session_store
|
|
180
|
+
|
|
181
|
+
session_store.create_thread(
|
|
182
|
+
po_name: name,
|
|
183
|
+
parent_po: parent_po,
|
|
184
|
+
parent_session_id: parent_session_id,
|
|
185
|
+
parent_message_id: parent_message_id,
|
|
186
|
+
thread_type: "delegation",
|
|
187
|
+
source: "delegation"
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Execute a message in a specific thread, then restore the original session.
|
|
192
|
+
# @param message [String, Hash] The message to process
|
|
193
|
+
# @param context [Context] Execution context
|
|
194
|
+
# @param thread_id [String] Thread ID to execute in
|
|
195
|
+
# @return [String] The response
|
|
196
|
+
def receive_in_thread(message, context:, thread_id:)
|
|
197
|
+
original_session = @session_id
|
|
198
|
+
original_history = @history.dup
|
|
199
|
+
|
|
200
|
+
# Switch to delegation thread
|
|
201
|
+
@session_id = thread_id
|
|
202
|
+
@history = []
|
|
203
|
+
reload_history_from_session
|
|
204
|
+
|
|
205
|
+
begin
|
|
206
|
+
result = receive(message, context: context)
|
|
207
|
+
result
|
|
208
|
+
ensure
|
|
209
|
+
# Restore original session
|
|
210
|
+
@session_id = original_session
|
|
211
|
+
@history = original_history
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Create a new root thread and switch to it.
|
|
216
|
+
# @param name [String, nil] Optional thread name
|
|
217
|
+
# @return [String] New thread ID
|
|
218
|
+
def new_thread(name: nil)
|
|
219
|
+
return nil unless session_store
|
|
220
|
+
|
|
221
|
+
@session_id = session_store.create_thread(
|
|
222
|
+
po_name: self.name,
|
|
223
|
+
name: name,
|
|
224
|
+
thread_type: "root"
|
|
225
|
+
)
|
|
226
|
+
@history = []
|
|
227
|
+
@session_id
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
private
|
|
231
|
+
|
|
232
|
+
def normalize_message(message)
|
|
233
|
+
case message
|
|
234
|
+
when String
|
|
235
|
+
message
|
|
236
|
+
when Hash
|
|
237
|
+
message[:message] || message["message"] || message.to_s
|
|
238
|
+
else
|
|
239
|
+
message.to_s
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def available_tool_descriptors
|
|
244
|
+
# Get declared capabilities from config
|
|
245
|
+
declared = @config["capabilities"] || []
|
|
246
|
+
|
|
247
|
+
# Add universal capabilities (available to all POs)
|
|
248
|
+
all_caps = declared + UNIVERSAL_CAPABILITIES
|
|
249
|
+
|
|
250
|
+
all_caps.filter_map do |cap_name|
|
|
251
|
+
cap = @env.registry&.get(cap_name)
|
|
252
|
+
cap&.descriptor
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def build_system_prompt
|
|
257
|
+
# Build context about this PO's identity
|
|
258
|
+
declared_caps = @config["capabilities"] || []
|
|
259
|
+
all_caps = declared_caps + UNIVERSAL_CAPABILITIES
|
|
260
|
+
|
|
261
|
+
context_block = <<~CONTEXT
|
|
262
|
+
## System Context
|
|
263
|
+
|
|
264
|
+
You are a prompt object named "#{name}".
|
|
265
|
+
When using tools that target a PO (like add_capability), you can use "self" or "#{name}" to target yourself.
|
|
266
|
+
|
|
267
|
+
Your declared capabilities: #{declared_caps.empty? ? '(none)' : declared_caps.join(', ')}
|
|
268
|
+
Universal capabilities (always available): #{UNIVERSAL_CAPABILITIES.join(', ')}
|
|
269
|
+
CONTEXT
|
|
270
|
+
|
|
271
|
+
"#{@body}\n\n#{context_block}"
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def execute_tool_calls(tool_calls, context)
|
|
275
|
+
# Track the caller for nested calls
|
|
276
|
+
previous_capability = context.current_capability
|
|
277
|
+
previous_calling_po = context.calling_po
|
|
278
|
+
|
|
279
|
+
tool_calls.map do |tc|
|
|
280
|
+
capability = @env.registry&.get(tc.name)
|
|
281
|
+
|
|
282
|
+
if capability
|
|
283
|
+
# Log the outgoing message
|
|
284
|
+
@env.bus.publish(from: name, to: tc.name, message: tc.arguments)
|
|
285
|
+
|
|
286
|
+
# Set context for the tool call
|
|
287
|
+
# calling_po tracks which PO is making the call (for "self" resolution)
|
|
288
|
+
context.calling_po = name
|
|
289
|
+
context.current_capability = tc.name
|
|
290
|
+
|
|
291
|
+
result = if capability.is_a?(PromptObject)
|
|
292
|
+
# PO-to-PO call: create isolated delegation thread
|
|
293
|
+
execute_po_delegation(capability, tc, context)
|
|
294
|
+
else
|
|
295
|
+
# Primitive call: execute directly
|
|
296
|
+
capability.receive(tc.arguments, context: context)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Restore context
|
|
300
|
+
context.current_capability = previous_capability
|
|
301
|
+
context.calling_po = previous_calling_po
|
|
302
|
+
|
|
303
|
+
# Log the response
|
|
304
|
+
@env.bus.publish(from: tc.name, to: name, message: result)
|
|
305
|
+
|
|
306
|
+
{ tool_call_id: tc.id, name: tc.name, content: result }
|
|
307
|
+
else
|
|
308
|
+
{ tool_call_id: tc.id, name: tc.name, content: "Unknown capability: #{tc.name}" }
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Execute a delegation to another PO in an isolated thread.
|
|
314
|
+
# @param target_po [PromptObject] The PO to delegate to
|
|
315
|
+
# @param tool_call [ToolCall] The tool call details
|
|
316
|
+
# @param context [Context] Execution context
|
|
317
|
+
# @return [String] The response
|
|
318
|
+
def execute_po_delegation(target_po, tool_call, context)
|
|
319
|
+
# Create a delegation thread in the target PO
|
|
320
|
+
delegation_thread = target_po.create_delegation_thread(
|
|
321
|
+
parent_po: name,
|
|
322
|
+
parent_session_id: @session_id,
|
|
323
|
+
parent_message_id: get_last_message_id
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
if delegation_thread
|
|
327
|
+
# Execute in isolated thread
|
|
328
|
+
target_po.receive_in_thread(tool_call.arguments, context: context, thread_id: delegation_thread)
|
|
329
|
+
else
|
|
330
|
+
# Fallback: execute in target's current session (no session store)
|
|
331
|
+
target_po.receive(tool_call.arguments, context: context)
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Get the ID of the last message in the current session.
|
|
336
|
+
# @return [Integer, nil]
|
|
337
|
+
def get_last_message_id
|
|
338
|
+
return nil unless session_store && @session_id
|
|
339
|
+
|
|
340
|
+
messages = session_store.get_messages(@session_id)
|
|
341
|
+
messages.last&.dig(:id)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# --- Session Persistence Helpers ---
|
|
345
|
+
|
|
346
|
+
# Load existing session or create a new one.
|
|
347
|
+
def load_or_create_session
|
|
348
|
+
session = session_store.get_or_create_session(po_name: name)
|
|
349
|
+
@session_id = session[:id]
|
|
350
|
+
reload_history_from_session
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Reload history from the current session.
|
|
354
|
+
def reload_history_from_session
|
|
355
|
+
return unless session_store && @session_id
|
|
356
|
+
|
|
357
|
+
messages = session_store.get_messages(@session_id)
|
|
358
|
+
@history = messages.map { |msg| convert_db_message_to_history(msg) }
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Persist a message to the session store.
|
|
362
|
+
def persist_message(msg)
|
|
363
|
+
return unless session_store && @session_id
|
|
364
|
+
|
|
365
|
+
case msg[:role]
|
|
366
|
+
when :user
|
|
367
|
+
session_store.add_message(
|
|
368
|
+
session_id: @session_id,
|
|
369
|
+
role: :user,
|
|
370
|
+
content: msg[:content],
|
|
371
|
+
from_po: msg[:from]
|
|
372
|
+
)
|
|
373
|
+
when :assistant
|
|
374
|
+
# Serialize tool_calls if present
|
|
375
|
+
tool_calls_data = msg[:tool_calls]&.map do |tc|
|
|
376
|
+
{ id: tc.id, name: tc.name, arguments: tc.arguments }
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
session_store.add_message(
|
|
380
|
+
session_id: @session_id,
|
|
381
|
+
role: :assistant,
|
|
382
|
+
content: msg[:content],
|
|
383
|
+
tool_calls: tool_calls_data
|
|
384
|
+
)
|
|
385
|
+
when :tool
|
|
386
|
+
session_store.add_message(
|
|
387
|
+
session_id: @session_id,
|
|
388
|
+
role: :tool,
|
|
389
|
+
tool_results: msg[:results]
|
|
390
|
+
)
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Convert a database message row to history format.
|
|
395
|
+
def convert_db_message_to_history(db_msg)
|
|
396
|
+
case db_msg[:role]
|
|
397
|
+
when :user
|
|
398
|
+
{ role: :user, content: db_msg[:content], from: db_msg[:from_po] || "human" }
|
|
399
|
+
when :assistant
|
|
400
|
+
msg = { role: :assistant, content: db_msg[:content] }
|
|
401
|
+
if db_msg[:tool_calls]
|
|
402
|
+
# Reconstruct tool call objects from Hashes
|
|
403
|
+
msg[:tool_calls] = db_msg[:tool_calls].map do |tc|
|
|
404
|
+
LLM::ToolCall.from_hash(tc)
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
msg
|
|
408
|
+
when :tool
|
|
409
|
+
{ role: :tool, results: db_msg[:tool_results] || [] }
|
|
410
|
+
else
|
|
411
|
+
{ role: db_msg[:role], content: db_msg[:content] }
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptObjects
|
|
4
|
+
# Registry for all capabilities (both primitives and prompt objects).
|
|
5
|
+
# Provides lookup by name and generates tool descriptors for LLM calls.
|
|
6
|
+
class Registry
|
|
7
|
+
def initialize
|
|
8
|
+
@capabilities = {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Register a capability.
|
|
12
|
+
# @param capability [Capability] The capability to register
|
|
13
|
+
# @return [Capability] The registered capability
|
|
14
|
+
def register(capability)
|
|
15
|
+
@capabilities[capability.name] = capability
|
|
16
|
+
capability
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Unregister a capability by name.
|
|
20
|
+
# @param name [String] The capability name
|
|
21
|
+
# @return [Capability, nil] The removed capability or nil
|
|
22
|
+
def unregister(name)
|
|
23
|
+
@capabilities.delete(name.to_s)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Get a capability by name.
|
|
27
|
+
# @param name [String] The capability name
|
|
28
|
+
# @return [Capability, nil] The capability or nil if not found
|
|
29
|
+
def get(name)
|
|
30
|
+
@capabilities[name.to_s]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Check if a capability exists.
|
|
34
|
+
# @param name [String] The capability name
|
|
35
|
+
# @return [Boolean]
|
|
36
|
+
def exists?(name)
|
|
37
|
+
@capabilities.key?(name.to_s)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Get all registered capabilities.
|
|
41
|
+
# @return [Array<Capability>]
|
|
42
|
+
def all
|
|
43
|
+
@capabilities.values
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get all capability names.
|
|
47
|
+
# @return [Array<String>]
|
|
48
|
+
def names
|
|
49
|
+
@capabilities.keys
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Get only prompt objects.
|
|
53
|
+
# @return [Array<PromptObject>]
|
|
54
|
+
def prompt_objects
|
|
55
|
+
@capabilities.values.select { |c| c.is_a?(PromptObject) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Get only primitives.
|
|
59
|
+
# @return [Array<Primitive>]
|
|
60
|
+
def primitives
|
|
61
|
+
@capabilities.values.select { |c| c.is_a?(Primitive) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Check if a capability is a prompt object.
|
|
65
|
+
# @param name [String] The capability name
|
|
66
|
+
# @return [Boolean]
|
|
67
|
+
def prompt_object?(name)
|
|
68
|
+
cap = get(name)
|
|
69
|
+
cap.is_a?(PromptObject)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Get tool descriptors for a list of capability names.
|
|
73
|
+
# @param names [Array<String>] Capability names
|
|
74
|
+
# @return [Array<Hash>] Tool descriptors for LLM
|
|
75
|
+
def descriptors_for(names)
|
|
76
|
+
names.filter_map do |name|
|
|
77
|
+
cap = get(name)
|
|
78
|
+
cap&.descriptor
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Get tool descriptors for all capabilities.
|
|
83
|
+
# @return [Array<Hash>]
|
|
84
|
+
def all_descriptors
|
|
85
|
+
@capabilities.values.map(&:descriptor)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|