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,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptObjects
|
|
4
|
+
module Universal
|
|
5
|
+
# Universal capability to add capabilities to a prompt object at runtime.
|
|
6
|
+
# This allows dynamic extension of POs after primitives are created.
|
|
7
|
+
class AddCapability < Primitive
|
|
8
|
+
def name
|
|
9
|
+
"add_capability"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def description
|
|
13
|
+
"Add a capability to a prompt object, allowing it to use new tools. Can target self or another PO."
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def parameters
|
|
17
|
+
{
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
target: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "Name of the prompt object to add the capability to. Use 'self' for the current PO."
|
|
23
|
+
},
|
|
24
|
+
capability: {
|
|
25
|
+
type: "string",
|
|
26
|
+
description: "Name of the capability to add (must already exist in the registry)"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
required: ["target", "capability"]
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def receive(message, context:)
|
|
34
|
+
target = message[:target] || message["target"]
|
|
35
|
+
capability = message[:capability] || message["capability"]
|
|
36
|
+
|
|
37
|
+
# Resolve 'self' to the calling PO (not current_capability which is this tool)
|
|
38
|
+
target = context.calling_po if target == "self"
|
|
39
|
+
|
|
40
|
+
# Find the target PO
|
|
41
|
+
target_po = context.env.registry.get(target)
|
|
42
|
+
unless target_po
|
|
43
|
+
return "Error: Prompt object '#{target}' not found"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
unless target_po.is_a?(PromptObject)
|
|
47
|
+
return "Error: '#{target}' is not a prompt object (can only add capabilities to POs)"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Check if capability exists
|
|
51
|
+
unless context.env.registry.exists?(capability)
|
|
52
|
+
return "Error: Capability '#{capability}' does not exist"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Check if already has it
|
|
56
|
+
current_caps = target_po.config["capabilities"] || []
|
|
57
|
+
if current_caps.include?(capability)
|
|
58
|
+
return "'#{target}' already has the '#{capability}' capability"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Add the capability
|
|
62
|
+
target_po.config["capabilities"] ||= []
|
|
63
|
+
target_po.config["capabilities"] << capability
|
|
64
|
+
|
|
65
|
+
# Persist to file so it's available on restart
|
|
66
|
+
if target_po.save
|
|
67
|
+
"Added '#{capability}' to '#{target}' and saved to file. It can now use this capability."
|
|
68
|
+
else
|
|
69
|
+
"Added '#{capability}' to '#{target}' (in-memory only, could not save to file). It can now use this capability."
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptObjects
|
|
4
|
+
module Universal
|
|
5
|
+
# Universal capability to add primitives to the current PO.
|
|
6
|
+
# Focused version of add_capability specifically for primitives with better UX.
|
|
7
|
+
class AddPrimitive < Primitive
|
|
8
|
+
# Names of stdlib primitives (built into the framework)
|
|
9
|
+
STDLIB_PRIMITIVES = %w[read_file list_files write_file http_get].freeze
|
|
10
|
+
|
|
11
|
+
def name
|
|
12
|
+
"add_primitive"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def description
|
|
16
|
+
"Add a primitive (deterministic Ruby tool) to your capabilities. Use list_primitives first to see what's available."
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def parameters
|
|
20
|
+
{
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
primitive: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "Name of the primitive to add (e.g., 'read_file', 'write_file')"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
required: ["primitive"]
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def receive(message, context:)
|
|
33
|
+
primitive_name = message[:primitive] || message["primitive"]
|
|
34
|
+
|
|
35
|
+
# Find the calling PO
|
|
36
|
+
caller = context.calling_po
|
|
37
|
+
unless caller
|
|
38
|
+
return "Error: No calling PO context. This capability adds primitives to the current PO."
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
target_po = context.env.registry.get(caller)
|
|
42
|
+
unless target_po.is_a?(PromptObject)
|
|
43
|
+
return "Error: Could not find calling PO '#{caller}'."
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Check if primitive exists
|
|
47
|
+
primitive = context.env.registry.get(primitive_name)
|
|
48
|
+
unless primitive
|
|
49
|
+
return suggest_primitives(primitive_name, context)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
unless primitive.is_a?(Primitive)
|
|
53
|
+
return "Error: '#{primitive_name}' is not a primitive (it's a prompt object). Use add_capability for POs."
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Check if it's a universal capability (shouldn't be added explicitly)
|
|
57
|
+
if universal_primitive?(primitive)
|
|
58
|
+
return "Error: '#{primitive_name}' is a universal capability and is already available to all POs."
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Check if already has it
|
|
62
|
+
current_caps = target_po.config["capabilities"] || []
|
|
63
|
+
if current_caps.include?(primitive_name)
|
|
64
|
+
return "You already have the '#{primitive_name}' primitive."
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Add the primitive
|
|
68
|
+
target_po.config["capabilities"] ||= []
|
|
69
|
+
target_po.config["capabilities"] << primitive_name
|
|
70
|
+
|
|
71
|
+
# Persist to file so it's available on restart
|
|
72
|
+
if target_po.save
|
|
73
|
+
"Added '#{primitive_name}' to your capabilities and saved to file. You can now use it."
|
|
74
|
+
else
|
|
75
|
+
"Added '#{primitive_name}' to your capabilities (in-memory only). You can now use it."
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def suggest_primitives(name, context)
|
|
82
|
+
# Get all available primitives (excluding universal ones)
|
|
83
|
+
available = context.env.registry.primitives
|
|
84
|
+
.reject { |p| universal_primitive?(p) }
|
|
85
|
+
.map(&:name)
|
|
86
|
+
|
|
87
|
+
# Find similar names
|
|
88
|
+
suggestions = available.select { |n| n.include?(name) || name.include?(n) || levenshtein_similar?(n, name) }
|
|
89
|
+
|
|
90
|
+
if suggestions.any?
|
|
91
|
+
"Error: Primitive '#{name}' not found. Did you mean: #{suggestions.join(', ')}?"
|
|
92
|
+
else
|
|
93
|
+
"Error: Primitive '#{name}' not found. Available primitives: #{available.join(', ')}"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def universal_primitive?(primitive)
|
|
98
|
+
primitive.class.name&.start_with?("PromptObjects::Universal")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Simple check for similar strings (within 2 edits)
|
|
102
|
+
def levenshtein_similar?(a, b)
|
|
103
|
+
return true if a == b
|
|
104
|
+
return false if (a.length - b.length).abs > 2
|
|
105
|
+
|
|
106
|
+
# Simple approximation: check if they share most characters
|
|
107
|
+
common = (a.chars & b.chars).length
|
|
108
|
+
max_len = [a.length, b.length].max
|
|
109
|
+
common.to_f / max_len > 0.6
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptObjects
|
|
4
|
+
module Universal
|
|
5
|
+
# Universal capability to pause and ask the human a question.
|
|
6
|
+
# For now this is synchronous (blocking). In Phase 5 with the TUI,
|
|
7
|
+
# this will become async with a notification queue.
|
|
8
|
+
class AskHuman < Primitive
|
|
9
|
+
def name
|
|
10
|
+
"ask_human"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def description
|
|
14
|
+
"Pause and ask the human a question. Use this when you need confirmation, clarification, or input."
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def parameters
|
|
18
|
+
{
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
question: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "The question to ask the human"
|
|
24
|
+
},
|
|
25
|
+
options: {
|
|
26
|
+
type: "array",
|
|
27
|
+
items: { type: "string" },
|
|
28
|
+
description: "Optional list of choices to present"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
required: ["question"]
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def receive(message, context:)
|
|
36
|
+
question = message[:question] || message["question"]
|
|
37
|
+
options = message[:options] || message["options"]
|
|
38
|
+
|
|
39
|
+
# In TUI mode, use the human queue (non-blocking for UI)
|
|
40
|
+
if context.tui_mode && context.human_queue
|
|
41
|
+
return receive_tui(question, options, context)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# REPL mode - use stdin directly
|
|
45
|
+
receive_repl(question, options, context)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def receive_tui(question, options, context)
|
|
51
|
+
# Queue the request and wait for response
|
|
52
|
+
request = context.human_queue.enqueue(
|
|
53
|
+
capability: context.current_capability,
|
|
54
|
+
question: question,
|
|
55
|
+
options: options
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Log to message bus
|
|
59
|
+
context.bus.publish(
|
|
60
|
+
from: context.current_capability,
|
|
61
|
+
to: "human",
|
|
62
|
+
message: "[waiting] #{question}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Block this thread until human responds via UI
|
|
66
|
+
response = request.wait_for_response
|
|
67
|
+
|
|
68
|
+
# Log the response
|
|
69
|
+
context.bus.publish(
|
|
70
|
+
from: "human",
|
|
71
|
+
to: context.current_capability,
|
|
72
|
+
message: response
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
response
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def receive_repl(question, options, context)
|
|
79
|
+
puts
|
|
80
|
+
puts "┌─ #{context.current_capability} asks ──────────────────────────────────┐"
|
|
81
|
+
puts "│"
|
|
82
|
+
puts "│ #{question}"
|
|
83
|
+
puts "│"
|
|
84
|
+
|
|
85
|
+
if options && !options.empty?
|
|
86
|
+
options.each_with_index do |opt, i|
|
|
87
|
+
puts "│ [#{i + 1}] #{opt}"
|
|
88
|
+
end
|
|
89
|
+
puts "│"
|
|
90
|
+
puts "└──────────────────────────────────────────────────────────────┘"
|
|
91
|
+
print "Your choice (1-#{options.length}): "
|
|
92
|
+
|
|
93
|
+
choice = $stdin.gets&.chomp
|
|
94
|
+
index = choice.to_i - 1
|
|
95
|
+
|
|
96
|
+
if index >= 0 && index < options.length
|
|
97
|
+
options[index]
|
|
98
|
+
else
|
|
99
|
+
choice # Return raw input if not a valid index
|
|
100
|
+
end
|
|
101
|
+
else
|
|
102
|
+
puts "└──────────────────────────────────────────────────────────────┘"
|
|
103
|
+
print "Your answer: "
|
|
104
|
+
$stdin.gets&.chomp || ""
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module PromptObjects
|
|
6
|
+
module Universal
|
|
7
|
+
# Universal capability to create new capabilities at runtime.
|
|
8
|
+
# Can create both prompt objects (markdown) and primitives (Ruby code).
|
|
9
|
+
# This enables self-modification - the system can create new specialists and tools.
|
|
10
|
+
class CreateCapability < Primitive
|
|
11
|
+
def name
|
|
12
|
+
"create_capability"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def description
|
|
16
|
+
"Create a new capability. Use type='prompt_object' for LLM-backed specialists, or type='primitive' for deterministic Ruby tools."
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def parameters
|
|
20
|
+
{
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
type: {
|
|
24
|
+
type: "string",
|
|
25
|
+
enum: ["prompt_object", "primitive"],
|
|
26
|
+
description: "Type of capability: 'prompt_object' for LLM specialists, 'primitive' for Ruby tools"
|
|
27
|
+
},
|
|
28
|
+
name: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "Name for the capability (lowercase, underscores allowed)"
|
|
31
|
+
},
|
|
32
|
+
description: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "Brief description of what this capability does"
|
|
35
|
+
},
|
|
36
|
+
# For prompt_objects:
|
|
37
|
+
capabilities: {
|
|
38
|
+
type: "array",
|
|
39
|
+
items: { type: "string" },
|
|
40
|
+
description: "(prompt_object only) List of capabilities this PO can use"
|
|
41
|
+
},
|
|
42
|
+
identity: {
|
|
43
|
+
type: "string",
|
|
44
|
+
description: "(prompt_object only) Who is this prompt object? Their personality and role."
|
|
45
|
+
},
|
|
46
|
+
behavior: {
|
|
47
|
+
type: "string",
|
|
48
|
+
description: "(prompt_object only) How should this prompt object behave?"
|
|
49
|
+
},
|
|
50
|
+
# For primitives:
|
|
51
|
+
parameters_schema: {
|
|
52
|
+
type: "object",
|
|
53
|
+
description: "(primitive only) JSON Schema for the parameters this primitive accepts"
|
|
54
|
+
},
|
|
55
|
+
ruby_code: {
|
|
56
|
+
type: "string",
|
|
57
|
+
description: "(primitive only) Ruby code for the receive method body. Has access to 'message' (Hash) and 'context'."
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
required: ["type", "name", "description"]
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def receive(message, context:)
|
|
65
|
+
type = message[:type] || message["type"]
|
|
66
|
+
cap_name = message[:name] || message["name"]
|
|
67
|
+
|
|
68
|
+
# Validate name
|
|
69
|
+
unless cap_name && cap_name.match?(/\A[a-z][a-z0-9_]*\z/)
|
|
70
|
+
return "Error: Name must be lowercase letters, numbers, and underscores, starting with a letter."
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Check if already exists
|
|
74
|
+
if context.env.registry.exists?(cap_name)
|
|
75
|
+
return "Error: A capability named '#{cap_name}' already exists."
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
result = case type
|
|
79
|
+
when "prompt_object"
|
|
80
|
+
create_prompt_object(message, context)
|
|
81
|
+
when "primitive"
|
|
82
|
+
create_primitive(message, context)
|
|
83
|
+
else
|
|
84
|
+
return "Error: type must be 'prompt_object' or 'primitive'"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# If creation succeeded, add the new capability to the creating PO
|
|
88
|
+
if result && !result.start_with?("Error:")
|
|
89
|
+
added_msg = add_to_creator(cap_name, context)
|
|
90
|
+
result = "#{result} #{added_msg}" if added_msg
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
result
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
# Add the newly created capability to the PO that created it
|
|
99
|
+
# Returns a message if added, nil otherwise
|
|
100
|
+
def add_to_creator(cap_name, context)
|
|
101
|
+
creator_name = context.calling_po
|
|
102
|
+
return nil unless creator_name
|
|
103
|
+
|
|
104
|
+
creator_po = context.env.registry.get(creator_name)
|
|
105
|
+
return nil unless creator_po.is_a?(PromptObject)
|
|
106
|
+
|
|
107
|
+
# Add to the creator's capabilities if not already present
|
|
108
|
+
creator_po.config["capabilities"] ||= []
|
|
109
|
+
unless creator_po.config["capabilities"].include?(cap_name)
|
|
110
|
+
creator_po.config["capabilities"] << cap_name
|
|
111
|
+
# Persist the change to file
|
|
112
|
+
saved = creator_po.save ? " and saved" : ""
|
|
113
|
+
return "Also added '#{cap_name}' to #{creator_name}'s capabilities#{saved}."
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def create_prompt_object(message, context)
|
|
120
|
+
cap_name = message[:name] || message["name"]
|
|
121
|
+
description = message[:description] || message["description"]
|
|
122
|
+
capabilities = message[:capabilities] || message["capabilities"] || []
|
|
123
|
+
identity = message[:identity] || message["identity"] || "You are a helpful assistant."
|
|
124
|
+
behavior = message[:behavior] || message["behavior"] || "Help the user with their request."
|
|
125
|
+
|
|
126
|
+
# Build the markdown content
|
|
127
|
+
frontmatter = {
|
|
128
|
+
"name" => cap_name,
|
|
129
|
+
"description" => description,
|
|
130
|
+
"capabilities" => capabilities
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
body = <<~MARKDOWN
|
|
134
|
+
# #{cap_name.split('_').map(&:capitalize).join(' ')}
|
|
135
|
+
|
|
136
|
+
## Identity
|
|
137
|
+
|
|
138
|
+
#{identity}
|
|
139
|
+
|
|
140
|
+
## Behavior
|
|
141
|
+
|
|
142
|
+
#{behavior}
|
|
143
|
+
MARKDOWN
|
|
144
|
+
|
|
145
|
+
# to_yaml already includes leading "---\n", so we just need the closing ---
|
|
146
|
+
content = "#{frontmatter.to_yaml}---\n\n#{body}"
|
|
147
|
+
|
|
148
|
+
# Write to file
|
|
149
|
+
path = File.join(context.env.objects_dir, "#{cap_name}.md")
|
|
150
|
+
File.write(path, content, encoding: "UTF-8")
|
|
151
|
+
|
|
152
|
+
# Load into environment
|
|
153
|
+
context.env.load_prompt_object(path)
|
|
154
|
+
|
|
155
|
+
"Created prompt object '#{cap_name}' with capabilities: #{capabilities.join(', ')}. It's now available."
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def create_primitive(message, context)
|
|
159
|
+
cap_name = message[:name] || message["name"]
|
|
160
|
+
description = message[:description] || message["description"]
|
|
161
|
+
params_schema = message[:parameters_schema] || message["parameters_schema"] || {}
|
|
162
|
+
ruby_code = message[:ruby_code] || message["ruby_code"]
|
|
163
|
+
|
|
164
|
+
unless ruby_code
|
|
165
|
+
return "Error: ruby_code is required for primitives"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Generate the Ruby class
|
|
169
|
+
class_name = cap_name.split('_').map(&:capitalize).join
|
|
170
|
+
|
|
171
|
+
ruby_content = <<~RUBY
|
|
172
|
+
# frozen_string_literal: true
|
|
173
|
+
# Auto-generated primitive: #{cap_name}
|
|
174
|
+
|
|
175
|
+
module PromptObjects
|
|
176
|
+
module Primitives
|
|
177
|
+
class #{class_name} < Primitive
|
|
178
|
+
def name
|
|
179
|
+
"#{cap_name}"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def description
|
|
183
|
+
"#{description.gsub('"', '\\"')}"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def parameters
|
|
187
|
+
#{params_schema.inspect}
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def receive(message, context:)
|
|
191
|
+
#{ruby_code.gsub(/^/, ' ').strip}
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
RUBY
|
|
197
|
+
|
|
198
|
+
# Write to primitives directory (uses env.primitives_dir for sandbox support)
|
|
199
|
+
FileUtils.mkdir_p(context.env.primitives_dir)
|
|
200
|
+
path = File.join(context.env.primitives_dir, "#{cap_name}.rb")
|
|
201
|
+
File.write(path, ruby_content, encoding: "UTF-8")
|
|
202
|
+
|
|
203
|
+
# Load and register
|
|
204
|
+
begin
|
|
205
|
+
load(path)
|
|
206
|
+
klass = PromptObjects::Primitives.const_get(class_name)
|
|
207
|
+
context.env.registry.register(klass.new)
|
|
208
|
+
"Created primitive '#{cap_name}'. It's now available. File: #{path}"
|
|
209
|
+
rescue SyntaxError => e
|
|
210
|
+
File.delete(path) if File.exist?(path)
|
|
211
|
+
"Error: Invalid Ruby syntax - #{e.message}"
|
|
212
|
+
rescue StandardError => e
|
|
213
|
+
File.delete(path) if File.exist?(path)
|
|
214
|
+
"Error creating primitive: #{e.message}"
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module PromptObjects
|
|
6
|
+
module Universal
|
|
7
|
+
# Universal capability to create new primitives (deterministic Ruby code).
|
|
8
|
+
# This enables POs to create their own tools at runtime.
|
|
9
|
+
class CreatePrimitive < Primitive
|
|
10
|
+
def name
|
|
11
|
+
"create_primitive"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def description
|
|
15
|
+
"Create a new primitive (deterministic Ruby tool). The primitive will be saved to the environment and added to your capabilities."
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def parameters
|
|
19
|
+
{
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
name: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "Name for the primitive (lowercase, underscores allowed, e.g., 'parse_json')"
|
|
25
|
+
},
|
|
26
|
+
description: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "Brief description of what this primitive does"
|
|
29
|
+
},
|
|
30
|
+
code: {
|
|
31
|
+
type: "string",
|
|
32
|
+
description: "Ruby code for the primitive's receive method. Has access to 'message' (Hash with parameters) and 'context'. Should return a String result."
|
|
33
|
+
},
|
|
34
|
+
parameters_schema: {
|
|
35
|
+
type: "object",
|
|
36
|
+
description: "Optional JSON Schema for the parameters this primitive accepts"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
required: ["name", "description", "code"]
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def receive(message, context:)
|
|
44
|
+
prim_name = message[:name] || message["name"]
|
|
45
|
+
description = message[:description] || message["description"]
|
|
46
|
+
code = message[:code] || message["code"]
|
|
47
|
+
params_schema = message[:parameters_schema] || message["parameters_schema"] || default_params_schema
|
|
48
|
+
|
|
49
|
+
# Validate name
|
|
50
|
+
unless prim_name && prim_name.match?(/\A[a-z][a-z0-9_]*\z/)
|
|
51
|
+
return "Error: Name must be lowercase letters, numbers, and underscores, starting with a letter."
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Check if already exists
|
|
55
|
+
if context.env.registry.exists?(prim_name)
|
|
56
|
+
return "Error: A capability named '#{prim_name}' already exists. Use modify_primitive to update it."
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Validate code syntax
|
|
60
|
+
syntax_error = validate_syntax(code)
|
|
61
|
+
return "Error: Invalid Ruby syntax - #{syntax_error}" if syntax_error
|
|
62
|
+
|
|
63
|
+
# Generate the Ruby class
|
|
64
|
+
class_name = prim_name.split("_").map(&:capitalize).join
|
|
65
|
+
ruby_content = generate_ruby_class(class_name, prim_name, description, params_schema, code)
|
|
66
|
+
|
|
67
|
+
# Write to primitives directory
|
|
68
|
+
FileUtils.mkdir_p(context.env.primitives_dir)
|
|
69
|
+
path = File.join(context.env.primitives_dir, "#{prim_name}.rb")
|
|
70
|
+
File.write(path, ruby_content, encoding: "UTF-8")
|
|
71
|
+
|
|
72
|
+
# Load and register
|
|
73
|
+
begin
|
|
74
|
+
load(path)
|
|
75
|
+
klass = PromptObjects::Primitives.const_get(class_name)
|
|
76
|
+
context.env.registry.register(klass.new)
|
|
77
|
+
rescue SyntaxError => e
|
|
78
|
+
File.delete(path) if File.exist?(path)
|
|
79
|
+
return "Error: Invalid Ruby syntax in generated code - #{e.message}"
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
File.delete(path) if File.exist?(path)
|
|
82
|
+
return "Error creating primitive: #{e.message}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Auto-add to the creating PO
|
|
86
|
+
added_msg = add_to_caller(prim_name, context)
|
|
87
|
+
|
|
88
|
+
result = "Created primitive '#{prim_name}' at #{path}."
|
|
89
|
+
result += " #{added_msg}" if added_msg
|
|
90
|
+
result
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def default_params_schema
|
|
96
|
+
{
|
|
97
|
+
type: "object",
|
|
98
|
+
properties: {},
|
|
99
|
+
required: []
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def validate_syntax(code)
|
|
104
|
+
# Try to parse the code to check for syntax errors
|
|
105
|
+
eval("proc { #{code} }")
|
|
106
|
+
nil
|
|
107
|
+
rescue SyntaxError => e
|
|
108
|
+
e.message.sub(/^\(eval\):\d+: /, "")
|
|
109
|
+
rescue StandardError
|
|
110
|
+
nil # Other errors are OK at this stage
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def generate_ruby_class(class_name, prim_name, description, params_schema, code)
|
|
114
|
+
# Escape description for string literal
|
|
115
|
+
escaped_desc = description.gsub('\\', '\\\\\\\\').gsub('"', '\\"')
|
|
116
|
+
|
|
117
|
+
<<~RUBY
|
|
118
|
+
# frozen_string_literal: true
|
|
119
|
+
# Auto-generated primitive: #{prim_name}
|
|
120
|
+
# Created by PO at #{Time.now.iso8601}
|
|
121
|
+
|
|
122
|
+
module PromptObjects
|
|
123
|
+
module Primitives
|
|
124
|
+
class #{class_name} < Primitive
|
|
125
|
+
def name
|
|
126
|
+
"#{prim_name}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def description
|
|
130
|
+
"#{escaped_desc}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def parameters
|
|
134
|
+
#{params_schema.inspect}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def receive(message, context:)
|
|
138
|
+
#{indent_code(code, 10)}
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
RUBY
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def indent_code(code, spaces)
|
|
147
|
+
code.lines.map.with_index do |line, i|
|
|
148
|
+
i.zero? ? line.rstrip : (" " * spaces) + line.rstrip
|
|
149
|
+
end.join("\n")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def add_to_caller(prim_name, context)
|
|
153
|
+
caller_name = context.calling_po
|
|
154
|
+
return nil unless caller_name
|
|
155
|
+
|
|
156
|
+
caller_po = context.env.registry.get(caller_name)
|
|
157
|
+
return nil unless caller_po.is_a?(PromptObject)
|
|
158
|
+
|
|
159
|
+
caller_po.config["capabilities"] ||= []
|
|
160
|
+
unless caller_po.config["capabilities"].include?(prim_name)
|
|
161
|
+
caller_po.config["capabilities"] << prim_name
|
|
162
|
+
saved = caller_po.save ? " and saved to file" : ""
|
|
163
|
+
return "Added '#{prim_name}' to your capabilities#{saved}."
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
nil
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|