mysql_genius-core 0.7.1 → 0.7.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dfc2a8ccac7f377e556a0c5f51b843e97b4882f20a49b24ba41b5ab9e4623268
4
- data.tar.gz: 170b7fcfbdfab6e9aa20db0fef7dcbcf3d86018c8b41c35a0cbe927c3a8961c5
3
+ metadata.gz: 321f2469ac2b5213d704f7afd4ac6211a0a327d1c8d054c9fdb95843dd4b2bde
4
+ data.tar.gz: 62a5b202b55bf2ffa2c7d76b5c263c7685bd4d9de14f206903ae35604dd146ce
5
5
  SHA512:
6
- metadata.gz: 4111e15ebb74f4139c854b7710f745509939228e5b8f55013fec9b69b0ed40249453686bee6c17254205c0fe907ed544104fdc049971a618d7376ff122019388
7
- data.tar.gz: b2ddb49da235bfaac6ab6e75f9725d1f0162d6ded16ebf43dbd7e119d647c26cab3f12994213576636c5c8ee512722eb05ca3ee5ca9f229c594858b252051ff6
6
+ metadata.gz: 5666c4264a19bfddb7052233e3d02c9499fdbf7ea6ffb3b0e4fa60c1bea117de03a3a349cf399b716cb947733050ac053a5cc52c287d2798b17ed7f6160c6306
7
+ data.tar.gz: c5f99646c2c9da80629824366b5ee1a0dcf7710cf84bf06f2cfe8e783d4f42b1a3fd0d4635a9ddc1fd7ff29ea5c7e4e77734bd4f995fef47ea0a32a6174e4504
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.2
4
+
5
+ ### Added
6
+ - **Anthropic Messages API support** in `Core::Ai::Client` — detects `:x_api_key` auth style, sends `x-api-key` + `anthropic-version` headers, uses top-level `system` parameter and `content[0].text` response parsing.
7
+ - **`max_tokens` field** on `Core::Ai::Config` (default 4096) — sent in both OpenAI and Anthropic request bodies.
8
+ - **Copy response button** on all AI result sections in the shared dashboard template.
9
+ - **Dark mode CSS classes** for AI result sections (`mg-ai-section`, `mg-ai-danger`, `mg-ai-warning`, `mg-ai-info`) replacing hardcoded inline styles.
10
+ - **`capability?(:standalone_header)`** guard on the dashboard heading — hides it when the rendering adapter provides its own header.
11
+
3
12
  ## 0.7.1
4
13
 
5
14
  ### Fixed
@@ -27,12 +27,11 @@ module MysqlGenius
27
27
 
28
28
  raise NotConfigured, "AI is not configured" unless @config.enabled?
29
29
 
30
- body = {
31
- messages: messages,
32
- response_format: { type: "json_object" },
33
- temperature: temperature,
34
- }
35
- body[:model] = @config.model if @config.model && !@config.model.empty?
30
+ body = if anthropic?
31
+ build_anthropic_body(messages, temperature)
32
+ else
33
+ build_openai_body(messages, temperature)
34
+ end
36
35
 
37
36
  response = post_with_redirects(URI(@config.endpoint), body.to_json)
38
37
  parsed = JSON.parse(response.body)
@@ -41,7 +40,11 @@ module MysqlGenius
41
40
  raise ApiError, "AI API error: #{parsed["error"]["message"] || parsed["error"]}"
42
41
  end
43
42
 
44
- content = parsed.dig("choices", 0, "message", "content")
43
+ content = if anthropic?
44
+ parsed.dig("content", 0, "text")
45
+ else
46
+ parsed.dig("choices", 0, "message", "content")
47
+ end
45
48
  raise ApiError, "No content in AI response" if content.nil?
46
49
 
47
50
  parse_json_content(content)
@@ -49,6 +52,35 @@ module MysqlGenius
49
52
 
50
53
  private
51
54
 
55
+ def anthropic?
56
+ @config.auth_style == :x_api_key
57
+ end
58
+
59
+ def build_openai_body(messages, temperature)
60
+ body = {
61
+ messages: messages,
62
+ response_format: { type: "json_object" },
63
+ temperature: temperature,
64
+ }
65
+ body[:max_tokens] = @config.max_tokens if @config.max_tokens
66
+ body[:model] = @config.model if @config.model && !@config.model.empty?
67
+ body
68
+ end
69
+
70
+ def build_anthropic_body(messages, temperature)
71
+ system_text = messages.select { |m| m[:role] == "system" }.map { |m| m[:content] }.join("\n\n")
72
+ user_messages = messages.reject { |m| m[:role] == "system" }
73
+
74
+ body = {
75
+ messages: user_messages,
76
+ max_tokens: @config.max_tokens || 4096,
77
+ temperature: temperature,
78
+ }
79
+ body[:system] = system_text unless system_text.empty?
80
+ body[:model] = @config.model if @config.model && !@config.model.empty?
81
+ body
82
+ end
83
+
52
84
  def parse_json_content(content)
53
85
  JSON.parse(content)
54
86
  rescue JSON::ParserError
@@ -78,8 +110,12 @@ module MysqlGenius
78
110
 
79
111
  request = Net::HTTP::Post.new(uri)
80
112
  request["Content-Type"] = "application/json"
81
- if @config.auth_style == :bearer
113
+ case @config.auth_style
114
+ when :bearer
82
115
  request["Authorization"] = "Bearer #{@config.api_key}"
116
+ when :x_api_key
117
+ request["x-api-key"] = @config.api_key
118
+ request["anthropic-version"] = "2023-06-01"
83
119
  else
84
120
  request["api-key"] = @config.api_key
85
121
  end
@@ -26,10 +26,11 @@ module MysqlGenius
26
26
  :auth_style,
27
27
  :system_context,
28
28
  :domain_context,
29
+ :max_tokens,
29
30
  keyword_init: true,
30
31
  ) do
31
32
  def initialize(**kwargs)
32
- super(domain_context: "", **kwargs)
33
+ super(domain_context: "", max_tokens: 4096, **kwargs)
33
34
  freeze
34
35
  end
35
36
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module MysqlGenius
4
4
  module Core
5
- VERSION = "0.7.1"
5
+ VERSION = "0.7.2"
6
6
  end
7
7
  end
@@ -1,9 +1,11 @@
1
+ <% if capability?(:standalone_header) %>
1
2
  <div style="display:flex;align-items:center;justify-content:space-between;">
2
3
  <h4>&#128024; MySQLGenius</h4>
3
4
  <button class="mg-theme-toggle" id="mg-theme-btn" title="Toggle dark/light theme" onclick="(function(){var d=document.documentElement,t=d.getAttribute('data-theme')==='dark'?'light':'dark';d.setAttribute('data-theme',t);localStorage.setItem('mg-theme',t);document.getElementById('mg-theme-btn').textContent=t==='dark'?'\u2600\uFE0F':'\uD83C\uDF19';})()">
4
5
  <script>document.write(document.documentElement.getAttribute('data-theme')==='dark'?'\u2600\uFE0F':'\uD83C\uDF19')</script>
5
6
  </button>
6
7
  </div>
8
+ <% end %>
7
9
 
8
10
  <div class="mg-tabs">
9
11
  <button class="mg-tab active" data-tab="dashboard">Dashboard</button>
@@ -825,7 +827,7 @@
825
827
  });
826
828
 
827
829
  ajax('POST', ROUTES.optimize, data, function(resp) {
828
- el('optimize-content').innerHTML = formatMarkdown(resp.suggestions || 'No suggestions available.');
830
+ el('optimize-content').innerHTML = formatMarkdown(resp.suggestions || 'No suggestions available.') + copyButton('optimize-content');
829
831
  show(el('optimize-results'));
830
832
  explainOptimize.disabled = false;
831
833
  explainOptimize.innerHTML = '&#9889; AI Optimization';
@@ -1230,9 +1232,25 @@
1230
1232
  });
1231
1233
  }
1232
1234
 
1235
+ function copyButton(targetId) {
1236
+ return '<div style="text-align:right;margin-top:8px;"><button class="mg-btn mg-btn-outline-secondary mg-btn-sm mg-copy-ai" data-target="' + targetId + '">Copy response</button></div>';
1237
+ }
1238
+
1239
+ document.addEventListener('click', function(e) {
1240
+ var btn = e.target.closest('.mg-copy-ai');
1241
+ if (!btn) return;
1242
+ var target = el(btn.dataset.target);
1243
+ if (!target) return;
1244
+ var text = target.innerText || target.textContent;
1245
+ navigator.clipboard.writeText(text).then(function() {
1246
+ btn.textContent = 'Copied!';
1247
+ setTimeout(function() { btn.textContent = 'Copy response'; }, 2000);
1248
+ });
1249
+ });
1250
+
1233
1251
  function showAiQueryResult(title, html) {
1234
1252
  el('ai-query-title').innerHTML = '<strong>&#9889; ' + escHtml(title) + '</strong>';
1235
- el('ai-query-content').innerHTML = html;
1253
+ el('ai-query-content').innerHTML = html + copyButton('ai-query-content');
1236
1254
  show(el('ai-query-result'));
1237
1255
  }
1238
1256
 
@@ -1306,7 +1324,7 @@
1306
1324
  });
1307
1325
 
1308
1326
  aiCall(ROUTES.index_advisor, data, function(resp) {
1309
- el('optimize-content').innerHTML = formatMarkdown(resp.indexes || resp.raw || 'No suggestions.');
1327
+ el('optimize-content').innerHTML = formatMarkdown(resp.indexes || resp.raw || 'No suggestions.') + copyButton('optimize-content');
1310
1328
  show(el('optimize-results'));
1311
1329
  indexAdvisor.disabled = false;
1312
1330
  indexAdvisor.innerHTML = '&#9889; Index Advisor';
@@ -1329,7 +1347,7 @@
1329
1347
  hide(el('server-ai-result'));
1330
1348
  aiCall(ROUTES.root_cause, {}, function(data) {
1331
1349
  el('server-ai-title').innerHTML = '<strong>&#9889; Root Cause Analysis</strong>';
1332
- el('server-ai-content').innerHTML = formatMarkdown(data.diagnosis || data.raw || 'No diagnosis.');
1350
+ el('server-ai-content').innerHTML = formatMarkdown(data.diagnosis || data.raw || 'No diagnosis.') + copyButton('server-ai-content');
1333
1351
  show(el('server-ai-result'));
1334
1352
  rootCauseBtn.disabled = false;
1335
1353
  rootCauseBtn.innerHTML = '&#9889; Why is it slow?';
@@ -1354,7 +1372,7 @@
1354
1372
  hide(el('server-ai-result'));
1355
1373
  aiCall(ROUTES.anomaly_detection, {}, function(data) {
1356
1374
  el('server-ai-title').innerHTML = '<strong>&#9889; Query Health Report</strong>';
1357
- el('server-ai-content').innerHTML = formatMarkdown(data.report || data.raw || 'No anomalies detected.');
1375
+ el('server-ai-content').innerHTML = formatMarkdown(data.report || data.raw || 'No anomalies detected.') + copyButton('server-ai-content');
1358
1376
  show(el('server-ai-result'));
1359
1377
  anomalyBtn.disabled = false;
1360
1378
  anomalyBtn.innerHTML = '&#9889; Anomaly Detection';
@@ -1378,7 +1396,7 @@
1378
1396
  hide(el('schema-result'));
1379
1397
  var table = el('schema-table').value;
1380
1398
  aiCall(ROUTES.schema_review, { table: table }, function(data) {
1381
- el('schema-result-content').innerHTML = formatFindings(data.findings || data.raw || '');
1399
+ el('schema-result-content').innerHTML = formatFindings(data.findings || data.raw || '') + copyButton('schema-result-content');
1382
1400
  show(el('schema-result'));
1383
1401
  schemaBtn.disabled = false;
1384
1402
  schemaBtn.innerHTML = '&#9889; Analyze Schema';
@@ -1409,7 +1427,7 @@
1409
1427
  var level = (data.risk_level || '').toLowerCase();
1410
1428
  var badgeClass = level === 'critical' ? 'mg-badge-danger' : level === 'high' ? 'mg-badge-danger' : level === 'medium' ? 'mg-badge-warning' : 'mg-badge-info';
1411
1429
  el('migration-risk-badge').innerHTML = level ? '<span class="mg-badge ' + badgeClass + '" style="font-size:14px;padding:4px 12px;">Risk: ' + level.toUpperCase() + '</span>' : '';
1412
- el('migration-result-content').innerHTML = formatFindings(data.assessment || data.raw || '');
1430
+ el('migration-result-content').innerHTML = formatFindings(data.assessment || data.raw || '') + copyButton('migration-result-content');
1413
1431
  show(el('migration-result'));
1414
1432
  migrationBtn.disabled = false;
1415
1433
  migrationBtn.innerHTML = '&#9889; Assess Risk';
@@ -1565,19 +1583,12 @@
1565
1583
 
1566
1584
  if (!sections.length) return formatMarkdown(text);
1567
1585
 
1568
- var badgeColors = { danger: '#dc3545', warning: '#ffc107', info: '#17a2b8' };
1569
- var bgColors = { danger: '#fff5f5', warning: '#fffbeb', info: '#f0f9ff' };
1570
- var borderColors = { danger: '#f5c6cb', warning: '#ffeeba', info: '#bee5eb' };
1571
-
1572
1586
  return sections.map(function(sec) {
1573
1587
  var content = formatMarkdown(sec.lines.join('\n').trim());
1574
1588
  if (!content || content === '<br>') return '';
1575
- var badge = badgeColors[sec.severity] || badgeColors.info;
1576
- var bg = bgColors[sec.severity] || bgColors.info;
1577
- var border = borderColors[sec.severity] || borderColors.info;
1578
- return '<div class="mg-card mg-mb" style="border-left:4px solid ' + badge + ';background:' + bg + ';border-color:' + border + ';">' +
1579
- (sec.title ? '<div class="mg-card-header" style="background:transparent;border-bottom:1px solid ' + border + ';"><strong>' + escHtml(sec.title) + '</strong></div>' : '') +
1580
- '<div class="mg-card-body" style="font-size:13px;">' + content + '</div></div>';
1589
+ return '<div class="mg-ai-section mg-ai-' + sec.severity + ' mg-mb">' +
1590
+ (sec.title ? '<div class="mg-ai-section-header"><strong>' + escHtml(sec.title) + '</strong></div>' : '') +
1591
+ '<div class="mg-ai-section-body">' + content + '</div></div>';
1581
1592
  }).filter(function(s) { return s; }).join('');
1582
1593
  }
1583
1594
  })();
@@ -411,13 +411,13 @@
411
411
  // --- Explain ---
412
412
 
413
413
  // performance_schema DIGEST_TEXT uses normalized spacing that isn't valid SQL:
414
- // "SELECT COUNT ( * ) FROM `riders`" needs to become "SELECT COUNT(*) FROM `riders`"
415
- // "... IN ( ... )" "... IN (...)"
414
+ // "SELECT COUNT ( * ) FROM `riders`" -> needs to become "SELECT COUNT(*) FROM `riders`"
415
+ // "... IN ( ... )" -> "... IN (...)"
416
416
  // Also replaces placeholder ? with 1 so EXPLAIN can parse it.
417
417
  function normalizeDigestSql(sql) {
418
418
  return sql
419
- .replace(/\(\s+/g, '(') // "( " "("
420
- .replace(/\s+\)/g, ')') // " )" ")"
419
+ .replace(/\(\s+/g, '(') // "( " -> "("
420
+ .replace(/\s+\)/g, ')') // " )" -> ")"
421
421
  .replace(/\s*,\s*/g, ', ') // normalize comma spacing
422
422
  .replace(/\?/g, '1'); // replace ? placeholders with literal 1
423
423
  }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mysql_genius-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antarr Byrd
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-12 00:00:00.000000000 Z
11
+ date: 2026-04-13 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Shared library used by the mysql_genius Rails engine and the mysql_genius-desktop
14
14
  standalone app. Contains the SQL validator, query runner, database analyses, and