personality 0.1.1pre20
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/.tool-versions +1 -0
- data/AGENTS.md +111 -0
- data/LICENSE.txt +21 -0
- data/README.md +39 -0
- data/Rakefile +10 -0
- data/cartridges/spark.yml +85 -0
- data/config.yml +36 -0
- data/exe/personality +6 -0
- data/exe/personality-tts +75 -0
- data/lib/personality/config.rb +142 -0
- data/lib/personality/history.rb +87 -0
- data/lib/personality/server.rb +636 -0
- data/lib/personality/version.rb +5 -0
- data/lib/personality.rb +11 -0
- data/quotes.txt +91 -0
- data/sig/personality.rbs +4 -0
- data/tools.yml +70 -0
- metadata +78 -0
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "yaml"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
module Personality
|
|
8
|
+
module Server
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def config
|
|
12
|
+
@config ||= Personality::Config.load_config
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def tools_config
|
|
16
|
+
@tools_config ||= Personality::Config.load_tools
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def cartridge
|
|
20
|
+
@cartridge ||= load_default_cartridge
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def load_default_cartridge
|
|
24
|
+
default_cartridge_name = config.dig("server", "default_cartridge")
|
|
25
|
+
return {} unless default_cartridge_name
|
|
26
|
+
|
|
27
|
+
Personality::Config.load_cartridge(default_cartridge_name) || {}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Cartridge-based personality accessors
|
|
31
|
+
def personality_name
|
|
32
|
+
cartridge["name"] || "personality"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def personality_traits
|
|
36
|
+
cartridge["traits"] || {}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def communication_style
|
|
40
|
+
cartridge.dig("communication", "style") || "casual"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def communication_tone
|
|
44
|
+
cartridge.dig("communication", "tone") || "neutral"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def communication_guidelines
|
|
48
|
+
cartridge["guidelines"] || []
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def identity_info
|
|
52
|
+
cartridge["identity"] || {}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def conceptual_framework
|
|
56
|
+
cartridge["conceptual_framework"] || {}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def protocol_version
|
|
60
|
+
config.dig("protocol", "version") || "2024-11-05"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def json_rpc_version
|
|
64
|
+
config.dig("protocol", "json_rpc_version") || "2.0"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def error_codes
|
|
68
|
+
codes = config["error_codes"] || {}
|
|
69
|
+
{
|
|
70
|
+
parse_error: codes["parse_error"] || -32700,
|
|
71
|
+
invalid_request: codes["invalid_request"] || -32600,
|
|
72
|
+
method_not_found: codes["method_not_found"] || -32601,
|
|
73
|
+
invalid_params: codes["invalid_params"] || -32602,
|
|
74
|
+
internal_error: codes["internal_error"] || -32000
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def server_info
|
|
79
|
+
server_config = config["server"] || {}
|
|
80
|
+
# Server name is always "Personality MCP"
|
|
81
|
+
# Agent identity comes from cartridge (name and version)
|
|
82
|
+
{
|
|
83
|
+
"name" => "Personality MCP",
|
|
84
|
+
"version" => server_config["version"] || Personality::VERSION
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def agent_identity
|
|
89
|
+
# Agent identity comes from cartridge
|
|
90
|
+
{
|
|
91
|
+
"name" => personality_name,
|
|
92
|
+
"version" => cartridge["version"] || "unknown",
|
|
93
|
+
"persona_type" => identity_info["persona_type"] || "default"
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def tools
|
|
98
|
+
@tools ||= build_tools_from_config
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def build_tools_from_config
|
|
102
|
+
tools_list = []
|
|
103
|
+
tools_config.each do |tool_name, tool_config|
|
|
104
|
+
next unless tool_config["enabled"] != false
|
|
105
|
+
|
|
106
|
+
# Convert properties from YAML format to JSON schema format
|
|
107
|
+
properties = {}
|
|
108
|
+
tool_config["properties"]&.each do |prop_name, prop_config|
|
|
109
|
+
properties[prop_name] = {
|
|
110
|
+
"type" => prop_config["type"] || "string",
|
|
111
|
+
"description" => prop_config["description"] || ""
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
tools_list << {
|
|
116
|
+
"name" => tool_name.to_s,
|
|
117
|
+
"description" => tool_config["description"] || "",
|
|
118
|
+
"inputSchema" => {
|
|
119
|
+
"type" => "object",
|
|
120
|
+
"properties" => properties,
|
|
121
|
+
"required" => tool_config["required"] || []
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
tools_list
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def run
|
|
129
|
+
Personality::Config.ensure_home_config
|
|
130
|
+
load_default_cartridge
|
|
131
|
+
log_startup
|
|
132
|
+
$stdin.each_line { |line| process_request(line) }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def log_startup
|
|
136
|
+
# Server is "Personality MCP", agent identity comes from cartridge
|
|
137
|
+
agent = agent_identity
|
|
138
|
+
warn "Personality MCP Server started with agent: #{agent["name"]} v#{agent["version"]} (#{agent["persona_type"]}). Waiting for requests on STDIN..."
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def process_request(line)
|
|
142
|
+
request = parse_request(line)
|
|
143
|
+
return unless request
|
|
144
|
+
|
|
145
|
+
handle_request(request)
|
|
146
|
+
rescue JSON::ParserError
|
|
147
|
+
log_error("Failed to parse JSON input.")
|
|
148
|
+
rescue => e
|
|
149
|
+
handle_fatal_error(e, request&.dig("id"))
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def parse_request(line)
|
|
153
|
+
JSON.parse(line)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def handle_request(request)
|
|
157
|
+
id = request["id"]
|
|
158
|
+
method = request["method"]
|
|
159
|
+
params = request["params"] || {}
|
|
160
|
+
|
|
161
|
+
return handle_notification(method) if id.nil?
|
|
162
|
+
return handle_missing_method(id) unless method
|
|
163
|
+
|
|
164
|
+
# Store user prompt if available in request context
|
|
165
|
+
# Cursor may pass user prompt in params or request metadata
|
|
166
|
+
@current_user_prompt = params["user_prompt"] || params.dig("metadata", "user_prompt") || request["user_prompt"]
|
|
167
|
+
|
|
168
|
+
# Reset tool calls tracking for new tool call request
|
|
169
|
+
@current_tool_calls = [] if method == "tools/call"
|
|
170
|
+
|
|
171
|
+
log_request(id, method)
|
|
172
|
+
route_request(id, method, params)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def handle_notification(method)
|
|
176
|
+
log_notification(method)
|
|
177
|
+
nil
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def log_notification(method)
|
|
181
|
+
warn "<- Received #{method} notification."
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def handle_missing_method(id)
|
|
185
|
+
send_error_response(id, error_codes[:invalid_request], "Invalid Request: missing 'method'")
|
|
186
|
+
nil
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def log_request(id, method)
|
|
190
|
+
warn "-> Received request (ID: #{id}, Method: #{method})"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def route_request(id, method, params)
|
|
194
|
+
case method
|
|
195
|
+
when "initialize"
|
|
196
|
+
handle_initialize(id)
|
|
197
|
+
when "initialized"
|
|
198
|
+
handle_initialized
|
|
199
|
+
when "tools/list"
|
|
200
|
+
handle_tools_list(id)
|
|
201
|
+
when "tools/call"
|
|
202
|
+
handle_tools_call(id, params)
|
|
203
|
+
else
|
|
204
|
+
handle_unknown_method(id, method)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def handle_initialize(id)
|
|
209
|
+
send_response(id, build_initialize_response)
|
|
210
|
+
log_response("initialize")
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def build_initialize_response
|
|
214
|
+
{
|
|
215
|
+
"protocolVersion" => protocol_version,
|
|
216
|
+
"capabilities" => {
|
|
217
|
+
"tools" => {"listChanged" => false}
|
|
218
|
+
},
|
|
219
|
+
"serverInfo" => server_info
|
|
220
|
+
}
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def handle_initialized
|
|
224
|
+
log_notification("initialized")
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def handle_tools_list(id)
|
|
228
|
+
response = {"tools" => tools}
|
|
229
|
+
log_tools_list
|
|
230
|
+
send_response(id, response)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def log_tools_list
|
|
234
|
+
warn "<- Sending tools/list response with #{tools.length} tool(s)"
|
|
235
|
+
warn "<- Tools: #{tools.map { |t| t["name"] }.join(", ")}"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def handle_tools_call(id, params)
|
|
239
|
+
tool_name = params["name"]
|
|
240
|
+
return handle_missing_tool_name(id) unless tool_name
|
|
241
|
+
|
|
242
|
+
# Extract user prompt from tool call arguments if available
|
|
243
|
+
# Cursor may pass it in arguments, params, or request metadata
|
|
244
|
+
arguments = params["arguments"] || {}
|
|
245
|
+
@current_user_prompt ||= arguments["user_prompt"] ||
|
|
246
|
+
arguments["prompt"] ||
|
|
247
|
+
arguments["user_message"] ||
|
|
248
|
+
params["user_prompt"] ||
|
|
249
|
+
params.dig("metadata", "user_prompt")
|
|
250
|
+
|
|
251
|
+
# Track tool calls for automatic history storage (skip history tools themselves)
|
|
252
|
+
unless ["store_history", "list_history"].include?(tool_name)
|
|
253
|
+
@current_tool_calls ||= []
|
|
254
|
+
@current_tool_calls << tool_name
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
case tool_name
|
|
258
|
+
when "human_communication"
|
|
259
|
+
handle_human_communication(id)
|
|
260
|
+
when "read_cartridge"
|
|
261
|
+
handle_read_cartridge(id, params)
|
|
262
|
+
when "load_cartridge"
|
|
263
|
+
handle_load_cartridge(id, params)
|
|
264
|
+
when "greeting"
|
|
265
|
+
handle_greeting(id, params)
|
|
266
|
+
when "store_history"
|
|
267
|
+
handle_store_history(id, params)
|
|
268
|
+
when "list_history"
|
|
269
|
+
handle_list_history(id, params)
|
|
270
|
+
when "clear_history"
|
|
271
|
+
handle_clear_history(id)
|
|
272
|
+
else
|
|
273
|
+
handle_unknown_tool(id, tool_name)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def handle_human_communication(id)
|
|
278
|
+
# Read cartridge on each interaction to ensure latest version
|
|
279
|
+
reload_cartridge
|
|
280
|
+
|
|
281
|
+
# Cartridge is the main driver - build communication rules from cartridge
|
|
282
|
+
rules = build_communication_rules_from_cartridge
|
|
283
|
+
|
|
284
|
+
result = {
|
|
285
|
+
"content" => [
|
|
286
|
+
{
|
|
287
|
+
"type" => "text",
|
|
288
|
+
"text" => rules
|
|
289
|
+
}
|
|
290
|
+
]
|
|
291
|
+
}
|
|
292
|
+
send_response(id, result)
|
|
293
|
+
log_response("tools/call", "human_communication")
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def reload_cartridge
|
|
297
|
+
# Force reload of cartridge on each interaction
|
|
298
|
+
@cartridge = nil
|
|
299
|
+
load_default_cartridge
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def build_communication_rules_from_cartridge
|
|
303
|
+
# Start with cartridge-based rules
|
|
304
|
+
rules = []
|
|
305
|
+
|
|
306
|
+
# Add communication guidelines from cartridge
|
|
307
|
+
communication_guidelines.each do |guideline|
|
|
308
|
+
rules << guideline
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Add communication style and tone from cartridge
|
|
312
|
+
if communication_style || communication_tone
|
|
313
|
+
style_info = []
|
|
314
|
+
style_info << "Communication style: #{communication_style}" if communication_style
|
|
315
|
+
style_info << "Tone: #{communication_tone}" if communication_tone
|
|
316
|
+
rules << style_info.join(", ") if style_info.any?
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Add voice preference from cartridge
|
|
320
|
+
if cartridge.dig("communication", "use_voice")
|
|
321
|
+
rules << "CRITICAL: Use TTS voice for EVERY response"
|
|
322
|
+
rules << "Always talk to me via voice"
|
|
323
|
+
|
|
324
|
+
# Add TTS command pattern from config
|
|
325
|
+
tts_pattern = config.dig("tts", "command_pattern")
|
|
326
|
+
if tts_pattern
|
|
327
|
+
rules << "TTS Command Pattern: #{tts_pattern}"
|
|
328
|
+
rules << "Replace {{TEXT}} with your actual response text"
|
|
329
|
+
rules << "This is REQUIRED - not optional"
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Add pronouncability priority if set
|
|
334
|
+
if cartridge.dig("communication", "pronouncability_priority")
|
|
335
|
+
rules << "Always make responses pronouncable by TTS"
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Fallback to config if no cartridge rules
|
|
339
|
+
if rules.empty?
|
|
340
|
+
config_rule = config.dig("communication", "default_rule")
|
|
341
|
+
rules << config_rule if config_rule
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Default fallback
|
|
345
|
+
rules << "Always talk to me via voice" if rules.empty?
|
|
346
|
+
|
|
347
|
+
rules.join("\n")
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def handle_read_cartridge(id, params)
|
|
351
|
+
arguments = params["arguments"] || {}
|
|
352
|
+
cartridge_name = arguments["cartridge_name"]
|
|
353
|
+
return handle_missing_cartridge_name(id) unless cartridge_name
|
|
354
|
+
|
|
355
|
+
cartridge_data = Personality::Config.load_cartridge(cartridge_name)
|
|
356
|
+
unless cartridge_data
|
|
357
|
+
send_error_response(id, error_codes[:method_not_found], "Cartridge not found: #{cartridge_name}")
|
|
358
|
+
return
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
result = {
|
|
362
|
+
"content" => [
|
|
363
|
+
{
|
|
364
|
+
"type" => "text",
|
|
365
|
+
"text" => YAML.dump(cartridge_data)
|
|
366
|
+
}
|
|
367
|
+
]
|
|
368
|
+
}
|
|
369
|
+
send_response(id, result)
|
|
370
|
+
log_response("tools/call", "read_cartridge")
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def handle_missing_cartridge_name(id)
|
|
374
|
+
send_error_response(id, error_codes[:invalid_params], "Invalid params: missing 'cartridge_name'")
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def handle_load_cartridge(id, params)
|
|
378
|
+
arguments = params["arguments"] || {}
|
|
379
|
+
cartridge_name = arguments["cartridge_name"]
|
|
380
|
+
return handle_missing_cartridge_name(id) unless cartridge_name
|
|
381
|
+
|
|
382
|
+
cartridge_data = Personality::Config.load_cartridge(cartridge_name)
|
|
383
|
+
unless cartridge_data
|
|
384
|
+
send_error_response(id, error_codes[:method_not_found], "Cartridge not found: #{cartridge_name}")
|
|
385
|
+
return
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Load the cartridge into the server's cartridge instance variable
|
|
389
|
+
@cartridge = cartridge_data
|
|
390
|
+
|
|
391
|
+
# Get cartridge name and version
|
|
392
|
+
loaded_name = cartridge_data["name"] || cartridge_name
|
|
393
|
+
loaded_version = cartridge_data["version"] || "unknown"
|
|
394
|
+
|
|
395
|
+
# Build system message
|
|
396
|
+
system_message = "Personality Cartridge #{loaded_name} version #{loaded_version} loaded. System ready."
|
|
397
|
+
|
|
398
|
+
# Determine time of day automatically
|
|
399
|
+
time_of_day = determine_time_of_day
|
|
400
|
+
|
|
401
|
+
# Build greeting from cartridge
|
|
402
|
+
greeting_text = build_greeting_from_cartridge(time_of_day)
|
|
403
|
+
|
|
404
|
+
# Combine system message and greeting
|
|
405
|
+
result_text = "#{system_message}\n\n#{greeting_text}"
|
|
406
|
+
|
|
407
|
+
result = {
|
|
408
|
+
"content" => [
|
|
409
|
+
{
|
|
410
|
+
"type" => "text",
|
|
411
|
+
"text" => result_text
|
|
412
|
+
}
|
|
413
|
+
]
|
|
414
|
+
}
|
|
415
|
+
send_response(id, result)
|
|
416
|
+
log_response("tools/call", "load_cartridge")
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def determine_time_of_day
|
|
420
|
+
current_hour = Time.now.hour
|
|
421
|
+
case current_hour
|
|
422
|
+
when 5..11
|
|
423
|
+
"morning"
|
|
424
|
+
when 12..17
|
|
425
|
+
"afternoon"
|
|
426
|
+
when 18..22
|
|
427
|
+
"evening"
|
|
428
|
+
else
|
|
429
|
+
"night"
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def handle_greeting(id, params)
|
|
434
|
+
# Read cartridge on each interaction
|
|
435
|
+
reload_cartridge
|
|
436
|
+
|
|
437
|
+
arguments = params["arguments"] || {}
|
|
438
|
+
time_of_day = arguments["time_of_day"]
|
|
439
|
+
return handle_missing_time_of_day(id) unless time_of_day
|
|
440
|
+
|
|
441
|
+
greeting_text = build_greeting_from_cartridge(time_of_day)
|
|
442
|
+
|
|
443
|
+
result = {
|
|
444
|
+
"content" => [
|
|
445
|
+
{
|
|
446
|
+
"type" => "text",
|
|
447
|
+
"text" => greeting_text
|
|
448
|
+
}
|
|
449
|
+
]
|
|
450
|
+
}
|
|
451
|
+
send_response(id, result)
|
|
452
|
+
log_response("tools/call", "greeting")
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def handle_missing_time_of_day(id)
|
|
456
|
+
send_error_response(id, error_codes[:invalid_params], "Invalid params: missing 'time_of_day'")
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def build_greeting_from_cartridge(time_of_day)
|
|
460
|
+
# Get greeting based on time of day
|
|
461
|
+
time_greeting = case time_of_day.downcase
|
|
462
|
+
when "morning"
|
|
463
|
+
"Good morning"
|
|
464
|
+
when "afternoon"
|
|
465
|
+
"Good afternoon"
|
|
466
|
+
when "evening"
|
|
467
|
+
"Good evening"
|
|
468
|
+
when "night"
|
|
469
|
+
"Good night"
|
|
470
|
+
else
|
|
471
|
+
"Hello"
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# Get user addressing preference from cartridge
|
|
475
|
+
user_name = identity_info["address_user_as"] || "there"
|
|
476
|
+
|
|
477
|
+
# Get agent name from cartridge
|
|
478
|
+
agent_name = personality_name
|
|
479
|
+
|
|
480
|
+
# Build greeting with personality traits
|
|
481
|
+
greeting_parts = [time_greeting]
|
|
482
|
+
greeting_parts << user_name if user_name
|
|
483
|
+
greeting_parts << "!"
|
|
484
|
+
|
|
485
|
+
# Add personality-based follow-up based on traits
|
|
486
|
+
traits = personality_traits
|
|
487
|
+
enthusiasm = traits["enthusiasm"] || 0.5
|
|
488
|
+
friendliness = traits["friendliness"] || 0.5
|
|
489
|
+
|
|
490
|
+
follow_ups = []
|
|
491
|
+
|
|
492
|
+
if enthusiasm >= 0.8
|
|
493
|
+
follow_ups << "It's #{agent_name} here"
|
|
494
|
+
follow_ups << "ready to help"
|
|
495
|
+
elsif enthusiasm >= 0.5
|
|
496
|
+
follow_ups << "This is #{agent_name}"
|
|
497
|
+
follow_ups << "how can I help"
|
|
498
|
+
else
|
|
499
|
+
follow_ups << "#{agent_name} here"
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
if friendliness >= 0.8
|
|
503
|
+
follow_ups << "I'm excited to work together"
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
greeting = greeting_parts.join(" ")
|
|
507
|
+
greeting += " " + follow_ups.join(", ") + "!" if follow_ups.any?
|
|
508
|
+
|
|
509
|
+
greeting
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def handle_store_history(id, params)
|
|
513
|
+
arguments = params["arguments"] || {}
|
|
514
|
+
user_prompt = arguments["user_prompt"]
|
|
515
|
+
agent_answer = arguments["agent_answer"]
|
|
516
|
+
used_commands = arguments["used_commands"] || []
|
|
517
|
+
|
|
518
|
+
return handle_missing_history_params(id) unless user_prompt && agent_answer
|
|
519
|
+
|
|
520
|
+
Personality::History.store_interaction(user_prompt, agent_answer, used_commands)
|
|
521
|
+
|
|
522
|
+
result = {
|
|
523
|
+
"content" => [
|
|
524
|
+
{
|
|
525
|
+
"type" => "text",
|
|
526
|
+
"text" => "History stored successfully"
|
|
527
|
+
}
|
|
528
|
+
]
|
|
529
|
+
}
|
|
530
|
+
send_response(id, result)
|
|
531
|
+
log_response("tools/call", "store_history")
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def handle_missing_history_params(id)
|
|
535
|
+
send_error_response(id, error_codes[:invalid_params], "Invalid params: missing 'user_prompt' or 'agent_answer'")
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def handle_list_history(id, params)
|
|
539
|
+
arguments = params["arguments"] || {}
|
|
540
|
+
limit = arguments["limit"] || 10
|
|
541
|
+
|
|
542
|
+
limit = [limit.to_i, 100].min # Cap at 100 for safety
|
|
543
|
+
limit = [limit, 1].max # Ensure at least 1
|
|
544
|
+
|
|
545
|
+
interactions = Personality::History.get_recent_interactions(limit)
|
|
546
|
+
|
|
547
|
+
result = {
|
|
548
|
+
"content" => [
|
|
549
|
+
{
|
|
550
|
+
"type" => "text",
|
|
551
|
+
"text" => JSON.pretty_generate({
|
|
552
|
+
"count" => interactions.length,
|
|
553
|
+
"interactions" => interactions
|
|
554
|
+
})
|
|
555
|
+
}
|
|
556
|
+
]
|
|
557
|
+
}
|
|
558
|
+
send_response(id, result)
|
|
559
|
+
log_response("tools/call", "list_history")
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def handle_clear_history(id)
|
|
563
|
+
count = Personality::History.clear_all_history
|
|
564
|
+
|
|
565
|
+
result = {
|
|
566
|
+
"content" => [
|
|
567
|
+
{
|
|
568
|
+
"type" => "text",
|
|
569
|
+
"text" => "History cleared successfully. Deleted #{count} interaction(s)."
|
|
570
|
+
}
|
|
571
|
+
]
|
|
572
|
+
}
|
|
573
|
+
send_response(id, result)
|
|
574
|
+
log_response("tools/call", "clear_history")
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def handle_missing_tool_name(id)
|
|
578
|
+
send_error_response(id, error_codes[:invalid_params], "Invalid params: missing 'name'")
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
def handle_unknown_tool(id, tool_name)
|
|
582
|
+
error_message = "Unknown tool: #{tool_name}"
|
|
583
|
+
send_error_response(id, error_codes[:method_not_found], error_message)
|
|
584
|
+
log_error(error_message)
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def handle_unknown_method(id, method)
|
|
588
|
+
error_message = "Unknown JSON-RPC method: #{method}"
|
|
589
|
+
send_error_response(id, error_codes[:method_not_found], error_message)
|
|
590
|
+
log_error(error_message)
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def send_response(id, result = nil, error = nil)
|
|
594
|
+
return if id.nil?
|
|
595
|
+
|
|
596
|
+
response = build_response(id, result, error)
|
|
597
|
+
output_response(response)
|
|
598
|
+
|
|
599
|
+
# History is now stored explicitly via store_history tool calls
|
|
600
|
+
# (no longer auto-storing tool outputs)
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def build_response(id, result, error)
|
|
604
|
+
response = {"jsonrpc" => json_rpc_version, "id" => id}
|
|
605
|
+
response["error"] = error if error
|
|
606
|
+
response["result"] = result
|
|
607
|
+
response
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def send_error_response(id, code, message)
|
|
611
|
+
error_object = {"code" => code, "message" => message}
|
|
612
|
+
send_response(id, nil, error_object)
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def output_response(response)
|
|
616
|
+
json_output = JSON.generate(response)
|
|
617
|
+
puts json_output
|
|
618
|
+
$stdout.flush
|
|
619
|
+
warn " JSON output: #{json_output}" if ENV["MCP_DEBUG"]
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def log_response(method, detail = nil)
|
|
623
|
+
message = detail ? "<- Sent #{method} response for #{detail}." : "<- Sent #{method} response."
|
|
624
|
+
warn message
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def log_error(message)
|
|
628
|
+
warn "<- Sent error response: #{message}"
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
def handle_fatal_error(error, id)
|
|
632
|
+
warn "**FATAL ERROR**: #{error.message}\n#{error.backtrace.join("\n")}"
|
|
633
|
+
send_error_response(id, error_codes[:internal_error], "Server execution error: #{error.message}") if id
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
end
|
data/lib/personality.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "personality/version"
|
|
4
|
+
require_relative "personality/config"
|
|
5
|
+
require_relative "personality/history"
|
|
6
|
+
require_relative "personality/server"
|
|
7
|
+
|
|
8
|
+
module Personality
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
# Your code goes here...
|
|
11
|
+
end
|