ruby_llm-agents 3.0.0 → 3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a72ac8e976da462e8a77ff8b50641acfab95fa061daacf1f38a70d5ca25dc80a
4
- data.tar.gz: 61b3595c0ef91fa70dfa776ef6deddce257c96098146a94deb902d0705a483bd
3
+ metadata.gz: 552fe8427ac0b47f41b14e4652ac65eac1c7b0389a603b71576f8171aba504f7
4
+ data.tar.gz: d356d7fc391f93194bc0d4dc1cd179178361af5db8c6b00270c8cbd9bcea19b8
5
5
  SHA512:
6
- metadata.gz: 0c3827e5607ba8376976a84048ae2a9486fe5797720c8842e371ad3793190aaa2a8ec6bea0015b94cbb1c34c8bd516502a6a052dca823e68b5291665d5ae63b8
7
- data.tar.gz: 6ac84e773905ee65d8ca0f482633247fb7254c64a13b3b25d83698503a9c70f70070a3ceb1127fc8d96271611e4270616030b1f64837c431b4553991b3038209
6
+ metadata.gz: 8d7d722cc72e9baa30a3da29f234ed039965f12743f541ae354f7d477aa7a9a5798c0639ddf37872fa1b67c58c7244b7490d0d9f548a03f3734e101a717153a9
7
+ data.tar.gz: 33f0e37172dbd4b607f86ca27d27b2be997b1650544dcbf41877dd00e094f66bde86b81afaa7aacbf31d7049eda2f9567f3a27b3970fad64c01113a6e436785b
@@ -74,7 +74,7 @@ module RubyLLM
74
74
  foreign_key: :execution_id, dependent: :destroy
75
75
 
76
76
  # Delegations so existing code keeps working transparently
77
- delegate :system_prompt, :user_prompt, :response, :error_message,
77
+ delegate :system_prompt, :user_prompt, :assistant_prompt, :response, :error_message,
78
78
  :messages_summary, :tool_calls, :attempts, :fallback_chain,
79
79
  :parameters, :routed_to, :classification_result,
80
80
  :cached_at, :cache_creation_tokens,
@@ -19,20 +19,151 @@
19
19
  })();
20
20
  </script>
21
21
 
22
+ <!-- Gruvbox-aware color palette: CSS custom properties swapped in .dark -->
23
+ <style>
24
+ :root {
25
+ /* Gray (Tailwind defaults) */
26
+ --c-gray-50: 249 250 251; --c-gray-100: 243 244 246; --c-gray-200: 229 231 235;
27
+ --c-gray-300: 209 213 219; --c-gray-400: 156 163 175; --c-gray-500: 107 114 128;
28
+ --c-gray-600: 75 85 99; --c-gray-700: 55 65 81; --c-gray-800: 31 41 55;
29
+ --c-gray-900: 17 24 39; --c-gray-950: 3 7 18;
30
+ /* Blue */
31
+ --c-blue-50: 239 246 255; --c-blue-100: 219 234 254; --c-blue-200: 191 219 254;
32
+ --c-blue-300: 147 197 253; --c-blue-400: 96 165 250; --c-blue-500: 59 130 246;
33
+ --c-blue-600: 37 99 235; --c-blue-700: 29 78 216; --c-blue-800: 30 64 175;
34
+ --c-blue-900: 30 58 138; --c-blue-950: 23 37 84;
35
+ /* Red */
36
+ --c-red-50: 254 242 242; --c-red-100: 254 226 226; --c-red-200: 254 202 202;
37
+ --c-red-300: 252 165 165; --c-red-400: 248 113 113; --c-red-500: 239 68 68;
38
+ --c-red-600: 220 38 38; --c-red-700: 185 28 28; --c-red-800: 153 27 27;
39
+ --c-red-900: 127 29 29; --c-red-950: 69 10 10;
40
+ /* Green */
41
+ --c-green-50: 240 253 244; --c-green-100: 220 252 231; --c-green-200: 187 247 208;
42
+ --c-green-300: 134 239 172; --c-green-400: 74 222 128; --c-green-500: 34 197 94;
43
+ --c-green-600: 22 163 74; --c-green-700: 21 128 61; --c-green-800: 22 101 52;
44
+ --c-green-900: 20 83 45; --c-green-950: 5 46 22;
45
+ /* Yellow */
46
+ --c-yellow-50: 254 252 232; --c-yellow-100: 254 249 195; --c-yellow-200: 254 240 138;
47
+ --c-yellow-300: 253 224 71; --c-yellow-400: 250 204 21; --c-yellow-500: 234 179 8;
48
+ --c-yellow-600: 202 138 4; --c-yellow-700: 161 98 7; --c-yellow-800: 133 77 14;
49
+ --c-yellow-900: 113 63 18; --c-yellow-950: 66 32 6;
50
+ /* Orange */
51
+ --c-orange-50: 255 247 237; --c-orange-100: 255 237 213; --c-orange-200: 254 215 170;
52
+ --c-orange-300: 253 186 116; --c-orange-400: 251 146 60; --c-orange-500: 249 115 22;
53
+ --c-orange-600: 234 88 12; --c-orange-700: 194 65 12; --c-orange-800: 154 52 18;
54
+ --c-orange-900: 124 45 18; --c-orange-950: 67 20 7;
55
+ /* Purple */
56
+ --c-purple-50: 250 245 255; --c-purple-100: 243 232 255; --c-purple-200: 233 213 255;
57
+ --c-purple-300: 216 180 254; --c-purple-400: 192 132 252; --c-purple-500: 168 85 247;
58
+ --c-purple-600: 147 51 234; --c-purple-700: 126 34 206; --c-purple-800: 107 33 168;
59
+ --c-purple-900: 88 28 135; --c-purple-950: 59 7 100;
60
+ /* Cyan */
61
+ --c-cyan-50: 236 254 255; --c-cyan-100: 207 250 254; --c-cyan-200: 165 243 252;
62
+ --c-cyan-300: 103 232 249; --c-cyan-400: 34 211 238; --c-cyan-500: 6 182 212;
63
+ --c-cyan-600: 8 145 178; --c-cyan-700: 14 116 144; --c-cyan-800: 21 94 117;
64
+ --c-cyan-900: 22 78 99; --c-cyan-950: 8 51 68;
65
+ /* Pink */
66
+ --c-pink-50: 253 242 248; --c-pink-100: 252 231 243; --c-pink-200: 251 207 232;
67
+ --c-pink-300: 249 168 212; --c-pink-400: 244 114 182; --c-pink-500: 236 72 153;
68
+ --c-pink-600: 219 39 119; --c-pink-700: 190 24 93; --c-pink-800: 157 23 77;
69
+ --c-pink-900: 131 24 67; --c-pink-950: 80 7 36;
70
+ /* Amber */
71
+ --c-amber-50: 255 251 235; --c-amber-100: 254 243 199; --c-amber-200: 253 230 138;
72
+ --c-amber-300: 252 211 77; --c-amber-400: 251 191 36; --c-amber-500: 245 158 11;
73
+ --c-amber-600: 217 119 6; --c-amber-700: 180 83 9; --c-amber-800: 146 64 14;
74
+ --c-amber-900: 120 53 15; --c-amber-950: 69 26 3;
75
+ }
76
+ .dark {
77
+ /* Gray → Gruvbox bg/fg */
78
+ --c-gray-50: 251 241 199; --c-gray-100: 235 219 178; --c-gray-200: 213 196 161;
79
+ --c-gray-300: 189 174 147; --c-gray-400: 189 174 147; --c-gray-500: 168 153 132;
80
+ --c-gray-600: 146 131 116; --c-gray-700: 102 92 84; --c-gray-800: 80 73 69;
81
+ --c-gray-900: 60 56 54; --c-gray-950: 40 40 40;
82
+ /* Blue → Gruvbox aqua (#83a598 / #458588) */
83
+ --c-blue-50: 195 215 205; --c-blue-100: 174 199 188; --c-blue-200: 153 182 170;
84
+ --c-blue-300: 131 165 152; --c-blue-400: 100 149 144; --c-blue-500: 69 133 136;
85
+ --c-blue-600: 58 112 115; --c-blue-700: 48 93 95; --c-blue-800: 38 73 75;
86
+ --c-blue-900: 28 55 57; --c-blue-950: 20 40 42;
87
+ /* Red → Gruvbox red (#fb4934 / #cc241d) */
88
+ --c-red-50: 253 165 155; --c-red-100: 253 135 122; --c-red-200: 252 104 87;
89
+ --c-red-300: 251 73 52; --c-red-400: 228 55 41; --c-red-500: 251 89 66;
90
+ --c-red-600: 204 36 29; --c-red-700: 136 24 19; --c-red-800: 102 18 14;
91
+ --c-red-900: 72 12 10; --c-red-950: 50 8 7;
92
+ /* Green → Gruvbox green (#b8bb26 / #98971a) */
93
+ --c-green-50: 225 227 145; --c-green-100: 213 215 112; --c-green-200: 199 201 75;
94
+ --c-green-300: 184 187 38; --c-green-400: 168 169 32; --c-green-500: 152 151 26;
95
+ --c-green-600: 126 125 22; --c-green-700: 101 100 17; --c-green-800: 77 77 13;
96
+ --c-green-900: 55 55 9; --c-green-950: 38 38 6;
97
+ /* Yellow → Gruvbox yellow (#fabd2f / #d79921) */
98
+ --c-yellow-50: 253 225 155; --c-yellow-100: 252 215 121; --c-yellow-200: 251 202 84;
99
+ --c-yellow-300: 250 189 47; --c-yellow-400: 233 171 40; --c-yellow-500: 215 153 33;
100
+ --c-yellow-600: 179 127 27; --c-yellow-700: 143 102 22; --c-yellow-800: 109 78 17;
101
+ --c-yellow-900: 77 55 12; --c-yellow-950: 53 38 8;
102
+ /* Orange → Gruvbox orange (#fe8019 / #d65d0e) */
103
+ --c-orange-50: 255 197 140; --c-orange-100: 255 172 102; --c-orange-200: 254 150 64;
104
+ --c-orange-300: 254 128 25; --c-orange-400: 234 111 20; --c-orange-500: 214 93 14;
105
+ --c-orange-600: 178 77 12; --c-orange-700: 142 62 9; --c-orange-800: 108 47 7;
106
+ --c-orange-900: 76 33 5; --c-orange-950: 52 23 3;
107
+ /* Purple → Gruvbox purple (#d3869b / #b16286) */
108
+ --c-purple-50: 235 186 200; --c-purple-100: 227 168 184; --c-purple-200: 219 151 170;
109
+ --c-purple-300: 211 134 155; --c-purple-400: 194 116 145; --c-purple-500: 177 98 134;
110
+ --c-purple-600: 147 81 111; --c-purple-700: 118 65 89; --c-purple-800: 90 50 68;
111
+ --c-purple-900: 63 35 48; --c-purple-950: 43 24 33;
112
+ /* Cyan → Gruvbox green-cyan (#8ec07c / #689d6a) */
113
+ --c-cyan-50: 194 228 184; --c-cyan-100: 177 216 164; --c-cyan-200: 160 204 144;
114
+ --c-cyan-300: 142 192 124; --c-cyan-400: 123 175 115; --c-cyan-500: 104 157 106;
115
+ --c-cyan-600: 86 131 88; --c-cyan-700: 69 105 70; --c-cyan-800: 53 80 54;
116
+ --c-cyan-900: 37 57 38; --c-cyan-950: 26 40 27;
117
+ /* Pink → Gruvbox purple (no pink in Gruvbox) */
118
+ --c-pink-50: 235 186 200; --c-pink-100: 227 168 184; --c-pink-200: 219 151 170;
119
+ --c-pink-300: 211 134 155; --c-pink-400: 194 116 145; --c-pink-500: 177 98 134;
120
+ --c-pink-600: 147 81 111; --c-pink-700: 118 65 89; --c-pink-800: 90 50 68;
121
+ --c-pink-900: 63 35 48; --c-pink-950: 43 24 33;
122
+ /* Amber → Gruvbox yellow-orange */
123
+ --c-amber-50: 254 224 155; --c-amber-100: 253 213 121; --c-amber-200: 252 198 84;
124
+ --c-amber-300: 251 184 47; --c-amber-400: 235 162 37; --c-amber-500: 215 140 26;
125
+ --c-amber-600: 179 116 22; --c-amber-700: 143 93 17; --c-amber-800: 109 71 13;
126
+ --c-amber-900: 77 50 9; --c-amber-950: 53 35 6;
127
+ }
128
+ </style>
129
+
22
130
  <!-- Tailwind CSS via CDN -->
23
131
  <script src="https://cdn.tailwindcss.com"></script>
24
132
 
25
133
  <script>
26
134
  tailwind.config = {
27
- darkMode: 'class'
135
+ darkMode: 'class',
136
+ theme: {
137
+ extend: {
138
+ colors: (function() {
139
+ var families = ['gray','blue','red','green','yellow','orange','purple','cyan','pink','amber'];
140
+ var shades = [50,100,200,300,400,500,600,700,800,900,950];
141
+ var c = {};
142
+ families.forEach(function(f) {
143
+ c[f] = {};
144
+ shades.forEach(function(s) {
145
+ c[f][s] = 'rgb(var(--c-' + f + '-' + s + ') / <alpha-value>)';
146
+ });
147
+ });
148
+ return c;
149
+ })()
150
+ }
151
+ }
28
152
  }
29
153
  </script>
30
154
 
31
155
  <!-- Highcharts for charts -->
32
156
  <script src="https://code.highcharts.com/highcharts.js"></script>
33
157
 
34
- <!-- Configure Highcharts defaults -->
158
+ <!-- Chart color helpers + Highcharts defaults -->
35
159
  <script>
160
+ function chartColor(lightHex, darkHex) {
161
+ return document.documentElement.classList.contains('dark') ? darkHex : lightHex;
162
+ }
163
+ function chartColorAlpha(lightRgba, darkR, darkG, darkB, alpha) {
164
+ return document.documentElement.classList.contains('dark')
165
+ ? 'rgba(' + darkR + ',' + darkG + ',' + darkB + ',' + alpha + ')' : lightRgba;
166
+ }
36
167
  Highcharts.setOptions({
37
168
  credits: { enabled: false },
38
169
  chart: {
@@ -41,22 +172,22 @@
41
172
  },
42
173
  title: { text: null },
43
174
  xAxis: {
44
- labels: { style: { color: '#6B7280' } },
45
- lineColor: 'rgba(107, 114, 128, 0.1)',
46
- tickColor: 'rgba(107, 114, 128, 0.1)'
175
+ labels: { style: { color: chartColor('#6B7280', '#928374') } },
176
+ lineColor: chartColorAlpha('rgba(107, 114, 128, 0.1)', 146, 131, 116, 0.1),
177
+ tickColor: chartColorAlpha('rgba(107, 114, 128, 0.1)', 146, 131, 116, 0.1)
47
178
  },
48
179
  yAxis: {
49
- labels: { style: { color: '#6B7280' } },
50
- gridLineColor: 'rgba(107, 114, 128, 0.1)'
180
+ labels: { style: { color: chartColor('#6B7280', '#928374') } },
181
+ gridLineColor: chartColorAlpha('rgba(107, 114, 128, 0.1)', 146, 131, 116, 0.1)
51
182
  },
52
183
  legend: {
53
- itemStyle: { color: '#6B7280' },
54
- itemHoverStyle: { color: '#9CA3AF' }
184
+ itemStyle: { color: chartColor('#6B7280', '#928374') },
185
+ itemHoverStyle: { color: chartColor('#9CA3AF', '#bdae93') }
55
186
  },
56
187
  tooltip: {
57
- backgroundColor: 'rgba(0, 0, 0, 0.85)',
188
+ backgroundColor: chartColorAlpha('rgba(0, 0, 0, 0.85)', 40, 40, 40, 0.95),
58
189
  borderColor: 'transparent',
59
- style: { color: '#E5E7EB', fontFamily: 'ui-monospace, monospace' }
190
+ style: { color: chartColor('#E5E7EB', '#ebdbb2'), fontFamily: 'ui-monospace, monospace' }
60
191
  }
61
192
  });
62
193
  </script>
@@ -109,7 +109,7 @@
109
109
  title: { text: null },
110
110
  xAxis: {
111
111
  type: 'datetime',
112
- labels: { style: { color: '#6B7280', fontSize: '9px', fontFamily: 'ui-monospace, monospace' }, format: '{value:%b %d}' },
112
+ labels: { style: { color: chartColor('#6B7280', '#928374'), fontSize: '9px', fontFamily: 'ui-monospace, monospace' }, format: '{value:%b %d}' },
113
113
  lineColor: 'transparent',
114
114
  tickLength: 0,
115
115
  gridLineWidth: 0
@@ -118,19 +118,19 @@
118
118
  title: { text: null },
119
119
  min: 0,
120
120
  allowDecimals: false,
121
- labels: { style: { color: '#6B7280', fontSize: '9px', fontFamily: 'ui-monospace, monospace' } },
122
- gridLineColor: 'rgba(107, 114, 128, 0.08)'
121
+ labels: { style: { color: chartColor('#6B7280', '#928374'), fontSize: '9px', fontFamily: 'ui-monospace, monospace' } },
122
+ gridLineColor: chartColorAlpha('rgba(107, 114, 128, 0.08)', 146, 131, 116, 0.08)
123
123
  },
124
124
  legend: { enabled: false },
125
125
  credits: { enabled: false },
126
126
  tooltip: {
127
- backgroundColor: 'rgba(0, 0, 0, 0.85)',
127
+ backgroundColor: chartColorAlpha('rgba(0, 0, 0, 0.85)', 40, 40, 40, 0.95),
128
128
  borderColor: 'transparent',
129
129
  borderRadius: 3,
130
- style: { color: '#E5E7EB', fontSize: '10px', fontFamily: 'ui-monospace, monospace' },
130
+ style: { color: chartColor('#E5E7EB', '#ebdbb2'), fontSize: '10px', fontFamily: 'ui-monospace, monospace' },
131
131
  shared: true,
132
132
  formatter: function() {
133
- let html = '<span style="color:#9CA3AF">' + Highcharts.dateFormat('%b %d', this.x) + '</span>';
133
+ let html = '<span style="color:' + chartColor('#9CA3AF', '#bdae93') + '">' + Highcharts.dateFormat('%b %d', this.x) + '</span>';
134
134
  let cost = trendData.find(d => new Date(d.date).getTime() === this.x);
135
135
  this.points.forEach(p => html += '<br/>' + p.series.name + ': <b>' + p.y + '</b>');
136
136
  if (cost) html += '<br/>cost: <b>$' + cost.cost.toFixed(4) + '</b>';
@@ -148,14 +148,14 @@
148
148
  {
149
149
  name: 'errors',
150
150
  data: errorData,
151
- color: '#EF4444',
152
- fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(239, 68, 68, 0.08)'], [1, 'rgba(239, 68, 68, 0)']] }
151
+ color: chartColor('#EF4444', '#fb4934'),
152
+ fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, chartColorAlpha('rgba(239, 68, 68, 0.08)', 251, 73, 52, 0.08)], [1, chartColorAlpha('rgba(239, 68, 68, 0)', 251, 73, 52, 0)]] }
153
153
  },
154
154
  {
155
155
  name: 'success',
156
156
  data: successData,
157
- color: '#10B981',
158
- fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(16, 185, 129, 0.08)'], [1, 'rgba(16, 185, 129, 0)']] }
157
+ color: chartColor('#10B981', '#b8bb26'),
158
+ fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, chartColorAlpha('rgba(16, 185, 129, 0.08)', 184, 187, 38, 0.08)], [1, chartColorAlpha('rgba(16, 185, 129, 0)', 184, 187, 38, 0)]] }
159
159
  }
160
160
  ]
161
161
  });
@@ -60,7 +60,7 @@
60
60
  type: 'datetime',
61
61
  min: now - getTimeRangeMs(range),
62
62
  max: now,
63
- labels: { style: { color: '#6B7280', fontSize: '9px', fontFamily: 'ui-monospace, monospace' }, format: '{value:' + fmt + '}' },
63
+ labels: { style: { color: chartColor('#6B7280', '#928374'), fontSize: '9px', fontFamily: 'ui-monospace, monospace' }, format: '{value:' + fmt + '}' },
64
64
  lineColor: 'transparent',
65
65
  tickLength: 0,
66
66
  gridLineWidth: 0
@@ -69,19 +69,19 @@
69
69
  title: { text: null },
70
70
  min: 0,
71
71
  allowDecimals: false,
72
- labels: { style: { color: '#6B7280', fontSize: '9px', fontFamily: 'ui-monospace, monospace' } },
73
- gridLineColor: 'rgba(107, 114, 128, 0.08)'
72
+ labels: { style: { color: chartColor('#6B7280', '#928374'), fontSize: '9px', fontFamily: 'ui-monospace, monospace' } },
73
+ gridLineColor: chartColorAlpha('rgba(107, 114, 128, 0.08)', 146, 131, 116, 0.08)
74
74
  },
75
75
  legend: { enabled: false },
76
76
  credits: { enabled: false },
77
77
  tooltip: {
78
- backgroundColor: 'rgba(0, 0, 0, 0.85)',
78
+ backgroundColor: chartColorAlpha('rgba(0, 0, 0, 0.85)', 40, 40, 40, 0.95),
79
79
  borderColor: 'transparent',
80
80
  borderRadius: 3,
81
- style: { color: '#E5E7EB', fontSize: '10px', fontFamily: 'ui-monospace, monospace' },
81
+ style: { color: chartColor('#E5E7EB', '#ebdbb2'), fontSize: '10px', fontFamily: 'ui-monospace, monospace' },
82
82
  shared: true,
83
83
  formatter: function() {
84
- let html = '<span style="color:#9CA3AF">' + Highcharts.dateFormat(fmt, this.x) + '</span>';
84
+ let html = '<span style="color:' + chartColor('#9CA3AF', '#bdae93') + '">' + Highcharts.dateFormat(fmt, this.x) + '</span>';
85
85
  this.points.forEach(p => html += '<br/>' + p.series.name + ': <b>' + p.y + '</b>');
86
86
  return html;
87
87
  }
@@ -97,14 +97,14 @@
97
97
  {
98
98
  name: 'errors',
99
99
  data: toDatetimePoints(data.series[1].data, range),
100
- color: '#EF4444',
101
- fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(239, 68, 68, 0.08)'], [1, 'rgba(239, 68, 68, 0)']] }
100
+ color: chartColor('#EF4444', '#fb4934'),
101
+ fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, chartColorAlpha('rgba(239, 68, 68, 0.08)', 251, 73, 52, 0.08)], [1, chartColorAlpha('rgba(239, 68, 68, 0)', 251, 73, 52, 0)]] }
102
102
  },
103
103
  {
104
104
  name: 'success',
105
105
  data: toDatetimePoints(data.series[0].data, range),
106
- color: '#10B981',
107
- fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(16, 185, 129, 0.08)'], [1, 'rgba(16, 185, 129, 0)']] }
106
+ color: chartColor('#10B981', '#b8bb26'),
107
+ fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, chartColorAlpha('rgba(16, 185, 129, 0.08)', 184, 187, 38, 0.08)], [1, chartColorAlpha('rgba(16, 185, 129, 0)', 184, 187, 38, 0)]] }
108
108
  }
109
109
  ]
110
110
  });
@@ -439,6 +439,19 @@
439
439
  <pre id="user-prompt-content" class="hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded p-4 text-xs overflow-x-auto max-h-96 font-mono whitespace-pre-wrap"><%= @execution.user_prompt %></pre>
440
440
  <% end %>
441
441
 
442
+ <!-- ── assistant prompt ──────────────────── -->
443
+ <% if @execution.respond_to?(:assistant_prompt) && @execution.assistant_prompt.present? %>
444
+ <div class="flex items-center gap-3 mt-6 mb-3">
445
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">assistant prompt</span>
446
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
447
+ <button type="button" onclick="togglePrompt('assistant')" class="font-mono text-xs text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
448
+ <span id="assistant-prompt-toggle">expand</span>
449
+ </button>
450
+ </div>
451
+ <p id="assistant-prompt-preview" class="text-xs text-gray-600 dark:text-gray-300 font-mono bg-gray-50 dark:bg-gray-900 rounded p-3 truncate"><%= @execution.assistant_prompt.truncate(150) %></p>
452
+ <pre id="assistant-prompt-content" class="hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded p-4 text-xs overflow-x-auto max-h-96 font-mono whitespace-pre-wrap"><%= @execution.assistant_prompt %></pre>
453
+ <% end %>
454
+
442
455
  <!-- ── conversation ──────────────────── -->
443
456
  <% if @execution.respond_to?(:messages_count) && @execution.messages_count.to_i > 0 %>
444
457
  <%
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddAssistantPromptToExecutionDetails < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ unless column_exists?(:ruby_llm_agents_execution_details, :assistant_prompt)
6
+ add_column :ruby_llm_agents_execution_details, :assistant_prompt, :text
7
+ end
8
+ end
9
+ end
@@ -9,7 +9,7 @@
9
9
  class SplitExecutionDetailsFromExecutions < ActiveRecord::Migration<%= migration_version %>
10
10
  # Columns that belong on execution_details, not executions
11
11
  DETAIL_COLUMNS = %i[
12
- error_message system_prompt user_prompt response messages_summary
12
+ error_message system_prompt user_prompt assistant_prompt response messages_summary
13
13
  tool_calls attempts fallback_chain parameters routed_to
14
14
  classification_result cached_at cache_creation_tokens
15
15
  ].freeze
@@ -50,6 +50,7 @@ class SplitExecutionDetailsFromExecutions < ActiveRecord::Migration<%= migration
50
50
  t.text :error_message
51
51
  t.text :system_prompt
52
52
  t.text :user_prompt
53
+ t.text :assistant_prompt
53
54
  t.json :response, default: {}
54
55
  t.json :messages_summary, default: {}, null: false
55
56
  t.json :tool_calls, default: [], null: false
@@ -60,6 +60,25 @@ module RubyLlmAgents
60
60
  )
61
61
  end
62
62
 
63
+ # Add assistant_prompt column to execution_details (v3.0 -> v3.1 upgrade)
64
+ def create_add_assistant_prompt_migration
65
+ if column_exists?(:ruby_llm_agents_execution_details, :assistant_prompt)
66
+ say_status :skip, "assistant_prompt column already exists on execution_details", :yellow
67
+ return
68
+ end
69
+
70
+ unless table_exists?(:ruby_llm_agents_execution_details)
71
+ say_status :skip, "execution_details table does not exist yet", :yellow
72
+ return
73
+ end
74
+
75
+ say_status :upgrade, "Adding assistant_prompt to execution_details", :blue
76
+ migration_template(
77
+ "add_assistant_prompt_migration.rb.tt",
78
+ File.join(db_migrate_path, "add_assistant_prompt_to_execution_details.rb")
79
+ )
80
+ end
81
+
63
82
  def suggest_config_consolidation
64
83
  ruby_llm_initializer = File.join(destination_root, "config/initializers/ruby_llm.rb")
65
84
  agents_initializer = File.join(destination_root, "config/initializers/ruby_llm_agents.rb")
@@ -116,7 +135,7 @@ module RubyLlmAgents
116
135
 
117
136
  # Detail columns that should only exist on execution_details, not executions
118
137
  DETAIL_COLUMNS = %i[
119
- error_message system_prompt user_prompt response messages_summary
138
+ error_message system_prompt user_prompt assistant_prompt response messages_summary
120
139
  tool_calls attempts fallback_chain parameters routed_to
121
140
  classification_result cached_at cache_creation_tokens
122
141
  ].freeze
@@ -380,7 +380,8 @@ module RubyLLM
380
380
  base_data.merge(
381
381
  model: model,
382
382
  system_prompt: system_prompt,
383
- user_prompt: user_prompt
383
+ user_prompt: user_prompt,
384
+ assistant_prompt: assistant_prompt
384
385
  )
385
386
  end
386
387
 
@@ -275,6 +275,9 @@ module RubyLLM
275
275
  system_prompt: config.persist_prompts ? stored_system_prompt : nil,
276
276
  user_prompt: config.persist_prompts ? stored_user_prompt : nil
277
277
  }
278
+ if config.persist_prompts && assistant_prompt_column_exists?
279
+ detail_data[:assistant_prompt] = stored_assistant_prompt
280
+ end
278
281
  detail_data.merge!(@_pending_detail_data) if @_pending_detail_data
279
282
  @_pending_detail_data = nil
280
283
 
@@ -528,7 +531,9 @@ module RubyLLM
528
531
  user_prompt: safe_user_prompt,
529
532
  messages_summary: config.persist_messages_summary ? messages_summary : {},
530
533
  error_message: error&.message
531
- }.merge(detail_fields || {})
534
+ }
535
+ detail_data[:assistant_prompt] = safe_assistant_prompt if assistant_prompt_column_exists?
536
+ detail_data.merge!(detail_fields || {})
532
537
 
533
538
  execution_data[:_detail_data] = detail_data
534
539
 
@@ -634,6 +639,23 @@ module RubyLLM
634
639
  nil
635
640
  end
636
641
 
642
+ # Safely captures assistant prompt, handling errors gracefully
643
+ #
644
+ # @return [String, nil] The assistant prompt or nil if unavailable
645
+ def safe_assistant_prompt
646
+ respond_to?(:assistant_prompt) ? assistant_prompt&.to_s : nil
647
+ rescue StandardError => e
648
+ Rails.logger.warn("[RubyLLM::Agents] Could not capture assistant_prompt: #{e.message}")
649
+ nil
650
+ end
651
+
652
+ # Returns the assistant prompt for storage
653
+ #
654
+ # @return [String, nil] The assistant prompt
655
+ def stored_assistant_prompt
656
+ safe_assistant_prompt
657
+ end
658
+
637
659
  # Safely extracts a value from response object
638
660
  #
639
661
  # @param response [Object] The response object
@@ -923,6 +945,22 @@ module RubyLLM
923
945
  end
924
946
  end
925
947
 
948
+ # Checks if the assistant_prompt column exists on execution_details
949
+ #
950
+ # Memoized to avoid repeated schema queries. Returns false for older installs
951
+ # that haven't run the migration yet.
952
+ #
953
+ # @return [Boolean] true if the column exists
954
+ def assistant_prompt_column_exists?
955
+ return @_assistant_prompt_column_exists if defined?(@_assistant_prompt_column_exists)
956
+
957
+ @_assistant_prompt_column_exists = begin
958
+ RubyLLM::Agents::ExecutionDetail.column_names.include?("assistant_prompt")
959
+ rescue StandardError
960
+ false
961
+ end
962
+ end
963
+
926
964
  # Emergency fallback to mark execution as failed
927
965
  #
928
966
  # Uses update_all to bypass ActiveRecord callbacks and validations,
@@ -4,6 +4,6 @@ module RubyLLM
4
4
  module Agents
5
5
  # Current version of the RubyLLM::Agents gem
6
6
  # @return [String] Semantic version string
7
- VERSION = "3.0.0"
7
+ VERSION = "3.1.0"
8
8
  end
9
9
  end
@@ -253,6 +253,13 @@ module RubyLLM
253
253
 
254
254
  detail_data = {}
255
255
 
256
+ if global_config.persist_prompts
257
+ exec_opts = context.options[:options] || {}
258
+ detail_data[:system_prompt] = exec_opts[:system_prompt]
259
+ detail_data[:user_prompt] = context.input.to_s.presence
260
+ detail_data[:assistant_prompt] = exec_opts[:assistant_prefill] if assistant_prompt_column_exists?
261
+ end
262
+
256
263
  if context.error
257
264
  detail_data[:error_message] = truncate_error_message(context.error.message)
258
265
  end
@@ -349,6 +356,12 @@ module RubyLLM
349
356
 
350
357
  # Store detail data for separate creation
351
358
  detail_data = { parameters: sanitize_parameters(context) }
359
+ if global_config.persist_prompts
360
+ exec_opts = context.options[:options] || {}
361
+ detail_data[:system_prompt] = exec_opts[:system_prompt]
362
+ detail_data[:user_prompt] = context.input.to_s.presence
363
+ detail_data[:assistant_prompt] = exec_opts[:assistant_prefill] if assistant_prompt_column_exists?
364
+ end
352
365
  detail_data[:error_message] = truncate_error_message(context.error.message) if context.error
353
366
  detail_data[:tool_calls] = context[:tool_calls] if context[:tool_calls].present?
354
367
  detail_data[:attempts] = context[:reliability_attempts] if context[:reliability_attempts].present?
@@ -496,6 +509,22 @@ module RubyLLM
496
509
  false
497
510
  end
498
511
 
512
+ # Checks if the assistant_prompt column exists on execution_details
513
+ #
514
+ # Memoized to avoid repeated schema queries.
515
+ #
516
+ # @return [Boolean]
517
+ def assistant_prompt_column_exists?
518
+ return @_assistant_prompt_column_exists if defined?(@_assistant_prompt_column_exists)
519
+
520
+ @_assistant_prompt_column_exists = begin
521
+ defined?(RubyLLM::Agents::ExecutionDetail) &&
522
+ RubyLLM::Agents::ExecutionDetail.column_names.include?("assistant_prompt")
523
+ rescue StandardError
524
+ false
525
+ end
526
+ end
527
+
499
528
  # Returns whether the Execution model is available
500
529
  #
501
530
  # @return [Boolean]
@@ -42,6 +42,7 @@ module RubyLLM
42
42
  # @return [Context] The context with tenant fields populated
43
43
  def call(context)
44
44
  resolve_tenant!(context)
45
+ ensure_tenant_record!(context)
45
46
  apply_api_configuration!(context)
46
47
  @app.call(context)
47
48
  end
@@ -84,6 +85,83 @@ module RubyLLM
84
85
  end
85
86
  end
86
87
 
88
+ # Ensures a Tenant record exists in the database for the resolved tenant.
89
+ #
90
+ # When a host model (e.g., Organization) with LLMTenant is passed as
91
+ # tenant: to an agent, the after_create callback only fires for new records.
92
+ # Pre-existing records won't have a Tenant row yet. This method auto-creates
93
+ # it on first use so budget tracking and the dashboard work correctly.
94
+ #
95
+ # @param context [Context] The execution context
96
+ def ensure_tenant_record!(context)
97
+ return unless context.tenant_id.present?
98
+ return unless tenant_table_exists?
99
+
100
+ tenant_object = context.tenant_object
101
+
102
+ # Only auto-create when the tenant object uses the LLMTenant concern
103
+ if tenant_object.respond_to?(:llm_tenant_id) && tenant_object.is_a?(::ActiveRecord::Base)
104
+ ensure_tenant_for_model!(tenant_object)
105
+ else
106
+ # For hash-based or string tenants, ensure a minimal record exists
107
+ RubyLLM::Agents::Tenant.find_or_create_by!(tenant_id: context.tenant_id)
108
+ end
109
+ rescue StandardError => e
110
+ # Don't fail the execution if tenant record creation fails
111
+ log_tenant_warning("ensure tenant record", e)
112
+ end
113
+
114
+ # Creates a Tenant record linked to the host model if one doesn't exist
115
+ #
116
+ # @param tenant_object [ActiveRecord::Base] The host model with LLMTenant
117
+ def ensure_tenant_for_model!(tenant_object)
118
+ # Check polymorphic link first, then tenant_id
119
+ existing = RubyLLM::Agents::Tenant.find_by(tenant_record: tenant_object) ||
120
+ RubyLLM::Agents::Tenant.find_by(tenant_id: tenant_object.llm_tenant_id)
121
+ return if existing
122
+
123
+ options = tenant_object.class.try(:llm_tenant_options) || {}
124
+ limits = options[:limits] || {}
125
+ name_method = options[:name] || :to_s
126
+
127
+ RubyLLM::Agents::Tenant.create!(
128
+ tenant_id: tenant_object.llm_tenant_id,
129
+ name: tenant_object.send(name_method).to_s,
130
+ tenant_record: tenant_object,
131
+ daily_limit: limits[:daily_cost],
132
+ monthly_limit: limits[:monthly_cost],
133
+ daily_token_limit: limits[:daily_tokens],
134
+ monthly_token_limit: limits[:monthly_tokens],
135
+ daily_execution_limit: limits[:daily_executions],
136
+ monthly_execution_limit: limits[:monthly_executions],
137
+ enforcement: options[:enforcement]&.to_s || "soft",
138
+ inherit_global_defaults: options.fetch(:inherit_global, true)
139
+ )
140
+ end
141
+
142
+ # Checks if the tenants table exists (memoized)
143
+ #
144
+ # @return [Boolean]
145
+ def tenant_table_exists?
146
+ return @tenant_table_exists if defined?(@tenant_table_exists)
147
+
148
+ @tenant_table_exists = ::ActiveRecord::Base.connection.table_exists?(:ruby_llm_agents_tenants)
149
+ rescue StandardError
150
+ @tenant_table_exists = false
151
+ end
152
+
153
+ # Logs a warning without failing the execution
154
+ #
155
+ # @param action [String] What was being attempted
156
+ # @param error [StandardError] The error
157
+ def log_tenant_warning(action, error)
158
+ return unless defined?(Rails) && Rails.respond_to?(:logger)
159
+
160
+ Rails.logger.warn(
161
+ "[RubyLLM::Agents] Failed to #{action}: #{error.message}"
162
+ )
163
+ end
164
+
87
165
  # Applies API configuration to RubyLLM based on resolved tenant
88
166
  #
89
167
  # @param context [Context] The execution context
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm-agents
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - adham90
@@ -146,6 +146,7 @@ files:
146
146
  - lib/generators/ruby_llm_agents/multi_tenancy_generator.rb
147
147
  - lib/generators/ruby_llm_agents/restructure_generator.rb
148
148
  - lib/generators/ruby_llm_agents/speaker_generator.rb
149
+ - lib/generators/ruby_llm_agents/templates/add_assistant_prompt_migration.rb.tt
149
150
  - lib/generators/ruby_llm_agents/templates/add_attempts_migration.rb.tt
150
151
  - lib/generators/ruby_llm_agents/templates/add_caching_migration.rb.tt
151
152
  - lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt