prompt_objects 0.2.0 → 0.3.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 +68 -0
- data/Gemfile.lock +1 -1
- data/README.md +2 -2
- data/exe/prompt_objects +387 -1
- data/frontend/src/App.tsx +11 -3
- data/frontend/src/components/ContextMenu.tsx +67 -0
- data/frontend/src/components/MessageBus.tsx +4 -3
- data/frontend/src/components/ModelSelector.tsx +5 -1
- data/frontend/src/components/ThreadsSidebar.tsx +46 -2
- data/frontend/src/components/UsagePanel.tsx +105 -0
- data/frontend/src/hooks/useWebSocket.ts +53 -0
- data/frontend/src/store/index.ts +10 -0
- data/frontend/src/types/index.ts +4 -1
- data/lib/prompt_objects/cli.rb +1 -0
- data/lib/prompt_objects/connectors/mcp.rb +1 -0
- data/lib/prompt_objects/environment.rb +24 -1
- data/lib/prompt_objects/llm/anthropic_adapter.rb +15 -1
- data/lib/prompt_objects/llm/factory.rb +93 -6
- data/lib/prompt_objects/llm/gemini_adapter.rb +13 -1
- data/lib/prompt_objects/llm/openai_adapter.rb +21 -4
- data/lib/prompt_objects/llm/pricing.rb +49 -0
- data/lib/prompt_objects/llm/response.rb +3 -2
- data/lib/prompt_objects/mcp/server.rb +1 -0
- data/lib/prompt_objects/message_bus.rb +27 -8
- data/lib/prompt_objects/prompt_object.rb +5 -3
- data/lib/prompt_objects/server/api/routes.rb +186 -29
- data/lib/prompt_objects/server/public/assets/index-Bkme6COu.css +1 -0
- data/lib/prompt_objects/server/public/assets/index-CQ7lVDF_.js +77 -0
- data/lib/prompt_objects/server/public/index.html +2 -2
- data/lib/prompt_objects/server/websocket_handler.rb +93 -9
- data/lib/prompt_objects/server.rb +54 -0
- data/lib/prompt_objects/session/store.rb +399 -4
- data/lib/prompt_objects.rb +1 -0
- data/prompt_objects.gemspec +1 -1
- data/templates/arc-agi-1/manifest.yml +22 -0
- data/templates/arc-agi-1/objects/data_manager.md +42 -0
- data/templates/arc-agi-1/objects/observer.md +100 -0
- data/templates/arc-agi-1/objects/solver.md +118 -0
- data/templates/arc-agi-1/objects/verifier.md +79 -0
- data/templates/arc-agi-1/primitives/check_arc_data.rb +53 -0
- data/templates/arc-agi-1/primitives/find_objects.rb +72 -0
- data/templates/arc-agi-1/primitives/grid_diff.rb +70 -0
- data/templates/arc-agi-1/primitives/grid_info.rb +42 -0
- data/templates/arc-agi-1/primitives/grid_transform.rb +50 -0
- data/templates/arc-agi-1/primitives/load_arc_task.rb +68 -0
- data/templates/arc-agi-1/primitives/render_grid.rb +78 -0
- data/templates/arc-agi-1/primitives/test_solution.rb +131 -0
- metadata +20 -3
- data/lib/prompt_objects/server/public/assets/index-CeNJvqLG.js +0 -77
- data/lib/prompt_objects/server/public/assets/index-Vx4-uMOU.css +0 -1
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>PromptObjects</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-CQ7lVDF_.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Bkme6COu.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body class="bg-po-bg text-gray-100">
|
|
12
12
|
<div id="root"></div>
|
|
@@ -121,7 +121,8 @@ module PromptObjects
|
|
|
121
121
|
payload: {
|
|
122
122
|
from: entry[:from],
|
|
123
123
|
to: entry[:to],
|
|
124
|
-
|
|
124
|
+
summary: entry[:summary],
|
|
125
|
+
content: serialize_bus_content(entry[:message]),
|
|
125
126
|
timestamp: entry[:timestamp].iso8601
|
|
126
127
|
}
|
|
127
128
|
)
|
|
@@ -175,7 +176,8 @@ module PromptObjects
|
|
|
175
176
|
payload: {
|
|
176
177
|
from: entry[:from],
|
|
177
178
|
to: entry[:to],
|
|
178
|
-
|
|
179
|
+
summary: entry[:summary],
|
|
180
|
+
content: serialize_bus_content(entry[:message]),
|
|
179
181
|
timestamp: entry[:timestamp].iso8601
|
|
180
182
|
}
|
|
181
183
|
)
|
|
@@ -221,6 +223,10 @@ module PromptObjects
|
|
|
221
223
|
handle_switch_llm(message["payload"])
|
|
222
224
|
when "update_prompt"
|
|
223
225
|
handle_update_prompt(message["payload"])
|
|
226
|
+
when "get_session_usage"
|
|
227
|
+
handle_get_session_usage(message["payload"])
|
|
228
|
+
when "export_thread"
|
|
229
|
+
handle_export_thread(message["payload"])
|
|
224
230
|
when "ping"
|
|
225
231
|
send_message(type: "pong", payload: {})
|
|
226
232
|
else
|
|
@@ -563,6 +569,63 @@ module PromptObjects
|
|
|
563
569
|
end
|
|
564
570
|
end
|
|
565
571
|
|
|
572
|
+
def handle_get_session_usage(payload)
|
|
573
|
+
session_id = payload["session_id"]
|
|
574
|
+
include_tree = payload["include_tree"] || false
|
|
575
|
+
return send_error("Session ID required") unless session_id
|
|
576
|
+
return send_error("No session store available") unless @runtime.session_store
|
|
577
|
+
|
|
578
|
+
usage = if include_tree
|
|
579
|
+
@runtime.session_store.thread_tree_usage(session_id)
|
|
580
|
+
else
|
|
581
|
+
@runtime.session_store.session_usage(session_id)
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# Convert by_model symbol keys to strings for JSON
|
|
585
|
+
by_model = {}
|
|
586
|
+
usage[:by_model].each { |model, data| by_model[model.to_s] = data }
|
|
587
|
+
|
|
588
|
+
send_message(
|
|
589
|
+
type: "session_usage",
|
|
590
|
+
payload: {
|
|
591
|
+
session_id: session_id,
|
|
592
|
+
include_tree: include_tree,
|
|
593
|
+
input_tokens: usage[:input_tokens],
|
|
594
|
+
output_tokens: usage[:output_tokens],
|
|
595
|
+
total_tokens: usage[:total_tokens],
|
|
596
|
+
estimated_cost_usd: usage[:estimated_cost_usd].round(6),
|
|
597
|
+
calls: usage[:calls],
|
|
598
|
+
by_model: by_model
|
|
599
|
+
}
|
|
600
|
+
)
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def handle_export_thread(payload)
|
|
604
|
+
session_id = payload["session_id"]
|
|
605
|
+
format = payload["format"] || "markdown"
|
|
606
|
+
return send_error("Session ID required") unless session_id
|
|
607
|
+
return send_error("No session store available") unless @runtime.session_store
|
|
608
|
+
|
|
609
|
+
content = case format
|
|
610
|
+
when "markdown"
|
|
611
|
+
@runtime.session_store.export_thread_tree_markdown(session_id)
|
|
612
|
+
when "json"
|
|
613
|
+
data = @runtime.session_store.export_thread_tree_json(session_id)
|
|
614
|
+
data ? JSON.pretty_generate(data) : nil
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
return send_error("Session not found") unless content
|
|
618
|
+
|
|
619
|
+
send_message(
|
|
620
|
+
type: "thread_export",
|
|
621
|
+
payload: {
|
|
622
|
+
session_id: session_id,
|
|
623
|
+
format: format,
|
|
624
|
+
content: content
|
|
625
|
+
}
|
|
626
|
+
)
|
|
627
|
+
end
|
|
628
|
+
|
|
566
629
|
# === Helpers ===
|
|
567
630
|
|
|
568
631
|
def send_error(message)
|
|
@@ -649,21 +712,29 @@ module PromptObjects
|
|
|
649
712
|
def message_to_hash(msg)
|
|
650
713
|
case msg[:role]
|
|
651
714
|
when :user
|
|
652
|
-
|
|
715
|
+
# In-memory messages use :from, SQLite-loaded messages use :from_po
|
|
716
|
+
from = msg[:from] || msg[:from_po]
|
|
717
|
+
{ role: "user", content: msg[:content], from: from }
|
|
653
718
|
when :assistant
|
|
654
719
|
hash = { role: "assistant", content: msg[:content] }
|
|
655
720
|
if msg[:tool_calls]
|
|
656
721
|
hash[:tool_calls] = msg[:tool_calls].map do |tc|
|
|
657
|
-
# Handle both ToolCall objects and Hashes
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
722
|
+
# Handle both ToolCall objects and Hashes (from DB with symbol or string keys)
|
|
723
|
+
if tc.is_a?(LLM::ToolCall)
|
|
724
|
+
{ id: tc.id, name: tc.name, arguments: tc.arguments }
|
|
725
|
+
else
|
|
726
|
+
tc_id = tc[:id] || tc["id"]
|
|
727
|
+
tc_name = tc[:name] || tc["name"]
|
|
728
|
+
tc_args = tc[:arguments] || tc["arguments"] || {}
|
|
729
|
+
{ id: tc_id, name: tc_name, arguments: tc_args }
|
|
730
|
+
end
|
|
662
731
|
end
|
|
663
732
|
end
|
|
664
733
|
hash
|
|
665
734
|
when :tool
|
|
666
|
-
|
|
735
|
+
# In-memory messages use :results, SQLite-loaded messages use :tool_results
|
|
736
|
+
results = msg[:results] || msg[:tool_results]
|
|
737
|
+
{ role: "tool", results: results }
|
|
667
738
|
else
|
|
668
739
|
{ role: msg[:role].to_s, content: msg[:content] }
|
|
669
740
|
end
|
|
@@ -678,6 +749,19 @@ module PromptObjects
|
|
|
678
749
|
options: request.options || []
|
|
679
750
|
}
|
|
680
751
|
end
|
|
752
|
+
|
|
753
|
+
# Serialize bus message content for JSON transmission.
|
|
754
|
+
# Handles String, Hash, and other types gracefully.
|
|
755
|
+
def serialize_bus_content(message)
|
|
756
|
+
case message
|
|
757
|
+
when Hash
|
|
758
|
+
message
|
|
759
|
+
when String
|
|
760
|
+
message
|
|
761
|
+
else
|
|
762
|
+
message.to_s
|
|
763
|
+
end
|
|
764
|
+
end
|
|
681
765
|
end
|
|
682
766
|
end
|
|
683
767
|
end
|
|
@@ -69,6 +69,9 @@ module PromptObjects
|
|
|
69
69
|
file_watcher.start
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
# Write .server file for CLI discovery
|
|
73
|
+
server_file = write_server_file(env_path, host: host, port: port) if env_path
|
|
74
|
+
|
|
72
75
|
Async do |task|
|
|
73
76
|
endpoint = Async::HTTP::Endpoint.parse(url)
|
|
74
77
|
|
|
@@ -82,7 +85,58 @@ module PromptObjects
|
|
|
82
85
|
end
|
|
83
86
|
ensure
|
|
84
87
|
file_watcher&.stop
|
|
88
|
+
remove_server_file(server_file) if server_file
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Write a .server file so CLI clients can discover the running server.
|
|
93
|
+
# @param env_path [String] Path to the environment directory
|
|
94
|
+
# @param host [String] Server host
|
|
95
|
+
# @param port [Integer] Server port
|
|
96
|
+
# @return [String] Path to the .server file
|
|
97
|
+
def self.write_server_file(env_path, host:, port:)
|
|
98
|
+
return nil unless env_path
|
|
99
|
+
|
|
100
|
+
server_file = File.join(env_path, ".server")
|
|
101
|
+
data = {
|
|
102
|
+
pid: Process.pid,
|
|
103
|
+
host: host,
|
|
104
|
+
port: port,
|
|
105
|
+
started_at: Time.now.iso8601
|
|
106
|
+
}
|
|
107
|
+
File.write(server_file, JSON.generate(data))
|
|
108
|
+
server_file
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Remove the .server file on shutdown.
|
|
112
|
+
# @param path [String] Path to the .server file
|
|
113
|
+
def self.remove_server_file(path)
|
|
114
|
+
File.delete(path) if path && File.exist?(path)
|
|
115
|
+
rescue StandardError
|
|
116
|
+
# Ignore cleanup errors
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Read a .server file to discover a running server.
|
|
120
|
+
# Returns nil if no server is running or the file is stale.
|
|
121
|
+
# @param env_path [String] Path to the environment directory
|
|
122
|
+
# @return [Hash, nil] Server info with :host, :port, :pid
|
|
123
|
+
def self.read_server_file(env_path)
|
|
124
|
+
server_file = File.join(env_path, ".server")
|
|
125
|
+
return nil unless File.exist?(server_file)
|
|
126
|
+
|
|
127
|
+
data = JSON.parse(File.read(server_file), symbolize_names: true)
|
|
128
|
+
|
|
129
|
+
# Check if the process is still running
|
|
130
|
+
begin
|
|
131
|
+
Process.kill(0, data[:pid])
|
|
132
|
+
data
|
|
133
|
+
rescue Errno::ESRCH
|
|
134
|
+
# Process is dead, clean up stale file
|
|
135
|
+
File.delete(server_file)
|
|
136
|
+
nil
|
|
85
137
|
end
|
|
138
|
+
rescue StandardError
|
|
139
|
+
nil
|
|
86
140
|
end
|
|
87
141
|
|
|
88
142
|
# Handle file change events and broadcast to connected clients.
|