@saltcorn/agents 0.8.3 → 0.8.5

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.
package/agent-view.js CHANGED
@@ -49,11 +49,13 @@ const {
49
49
  get_initial_interactions,
50
50
  get_skill_instances,
51
51
  saveInteractions,
52
+ extractText,
52
53
  } = require("./common");
53
54
  const MarkdownIt = require("markdown-it"),
54
55
  md = new MarkdownIt({ html: true, breaks: true, linkify: true });
55
56
  const { isWeb, escapeHtml } = require("@saltcorn/data/utils");
56
57
  const path = require("path");
58
+ const fs = require("fs");
57
59
 
58
60
  const configuration_workflow = (req) =>
59
61
  new Workflow({
@@ -259,6 +261,11 @@ const realTimeCollabScript = (viewname, rndid, layout) => {
259
261
  );
260
262
  };
261
263
 
264
+ const agents_css = fs.readFileSync(
265
+ path.resolve(__dirname, "agents.css"),
266
+ "utf8",
267
+ );
268
+
262
269
  const run = async (
263
270
  table_id,
264
271
  viewname,
@@ -300,12 +307,13 @@ const run = async (
300
307
  },
301
308
  { orderBy: "started_at", orderDesc: true, limit: 30 },
302
309
  )
303
- ).filter((r) => r.context.interactions)
310
+ ).filter((r) => r.context.interactions || r.context.html_interactions)
304
311
  : null;
305
312
 
306
313
  const cfgMsg = incompleteCfgMsg();
307
314
  if (cfgMsg) return cfgMsg;
308
315
  let runInteractions = "";
316
+ let hasInputForm = true;
309
317
 
310
318
  const initial_q = state.run_id ? undefined : state._q;
311
319
  if (state.run_id) {
@@ -319,6 +327,9 @@ const run = async (
319
327
  const interactMarkups = [];
320
328
  if (run.context.html_interactions) {
321
329
  interactMarkups.push(...run.context.html_interactions);
330
+ // no input if interactions deleted
331
+ if (!run.context.interactions && run.context.html_interactions.length)
332
+ hasInputForm = false;
322
333
  } else
323
334
  for (const interact of run.context.interactions) {
324
335
  //legacy
@@ -590,11 +601,11 @@ const run = async (
590
601
  ),
591
602
  prevRuns.map((run) => {
592
603
  const isActive = state.run_id && +state.run_id === run.id;
593
- const preview = escapeHtml(
594
- run.context.interactions
595
- .find((ix) => typeof ix?.content === "string")
596
- ?.content?.substring?.(0, 80),
597
- );
604
+ const previewHtml =
605
+ (run.context.interactions || []).find(
606
+ (ix) => typeof ix?.content === "string",
607
+ )?.content || extractText(run.context.html_interactions[0] || "");
608
+ const preview = escapeHtml(previewHtml?.substring?.(0, 80));
598
609
  return isModernSidebar
599
610
  ? div(
600
611
  {
@@ -655,301 +666,8 @@ const run = async (
655
666
  ),
656
667
  div({ id: "copilotinteractions" }, runInteractions),
657
668
  stream ? div({ class: "next_response_scratch" }) : "",
658
- input_form,
659
- style(
660
- `div.interaction-segment:not(:first-child) {border-top: 1px solid #e7e7e7; }
661
- div.interaction-segment {padding-top: 5px;padding-bottom: 5px;}
662
- div.interaction-segment p {margin-bottom: 0px;}
663
- div.interaction-segment div.card {margin-top: 0.5rem;}
664
- div.interaction-segment.to-right {
665
- display: flex;
666
- flex-direction: row-reverse;
667
- }
668
- div.interaction-segment.to-right div.badgewrap {
669
- display: flex;
670
- flex-direction: row-reverse;
671
- }
672
- div.prevcopilotrun:hover {cursor: pointer; background-color: var(--tblr-secondary-bg-subtle, var(--bs-secondary-bg-subtle, gray));}
673
- div.prevcopilotrun i.fa-trash-alt {display: none;}
674
- div.prevcopilotrun:hover i.fa-trash-alt {display: block;}
675
- .copilot-entry .submit-button:hover { cursor: pointer}
676
- .copilot-entry span.attach_agent_image_wrap i:hover { cursor: pointer}
677
-
678
- .copilot-entry .submit-button {
679
- position: relative;
680
- top: -1.8rem;
681
- left: 0.1rem;
682
- }
683
- .copilot-entry #audioinputicon {
684
- position: relative;
685
- top: -1.8rem;
686
- right: 0.7rem;
687
- cursor: pointer;
688
- float: right;
689
- }
690
- .copilot-entry .debugicon {
691
- position: relative;
692
- top: -1.8rem;
693
- left: 0.1rem;
694
- cursor: pointer;
695
- }
696
- .copilot-entry .cancelbtn {
697
- position: relative;
698
- top: -1.8rem;
699
- left: 0.1rem;
700
- cursor: pointer;
701
- }
702
- .copilot-entry .skill-form-widget {
703
- position: relative;
704
- top: -2rem;
705
- left: 0.4rem;
706
- display: inline;
707
- }
708
- .session-open-sessions, .open-prev-runs {
709
- cursor: pointer;
710
- }
711
- .copilot-entry span.attach_agent_image_wrap {
712
- position: relative;
713
- top: -1.8rem;
714
- left: 0.2rem;
715
- }
716
- .copilot-entry.dragover {
717
- outline: 2px dashed var(--tblr-primary, #0054a6);
718
- outline-offset: -2px;
719
- background: var(--tblr-primary-bg-subtle, rgba(0, 84, 166, 0.05));
720
- border-radius: 0.25rem;
721
- }
722
- .copilot-entry .explainer {
723
- position: relative;
724
- top: -1.2rem;
725
- display: block;
726
- }
727
- .col-0 {
728
- width: 0%
729
- }
730
- .copilot-entry {margin-bottom: -1.25rem; margin-top: 1rem;}
731
- p.prevrun_content {
732
- white-space: nowrap;
733
- overflow: hidden;
734
- margin-bottom: 0px;
735
- display: block;
736
- text-overflow: ellipsis;}
737
- /* Typing / Waiting Indicator */
738
- .agent-waiting-indicator { display:flex; align-items:center; padding:0.75rem 1rem; }
739
- .typing-dots { display:flex; gap:4px; align-items:center; }
740
- .typing-dots span { width:8px; height:8px; border-radius:50%; background:#6c757d; animation:typingBounce 1.4s infinite ease-in-out both; }
741
- .typing-dots span:nth-child(1) { animation-delay:-0.32s; }
742
- .typing-dots span:nth-child(2) { animation-delay:-0.16s; }
743
- .typing-dots span:nth-child(3) { animation-delay:0s; }
744
- @keyframes typingBounce { 0%,80%,100%{transform:scale(.6);opacity:.4} 40%{transform:scale(1);opacity:1} }
745
- /* Modern Chat Layout */
746
- .modern-chat-layout {
747
- display: flex;
748
- flex-direction: column;
749
- height: 100%;
750
- }
751
- .modern-chat-layout #copilotinteractions {
752
- max-height: 70vh;
753
- overflow-y: auto;
754
- padding: 1rem;
755
- display: flex;
756
- flex-direction: column;
757
- gap: 0.75rem;
758
- }
759
- .modern-chat-layout .chat-message {
760
- display: flex;
761
- gap: 0.5rem;
762
- max-width: 85%;
763
- align-items: flex-start;
764
- }
765
- .modern-chat-layout .chat-message.chat-user {
766
- align-self: flex-end;
767
- flex-direction: row-reverse;
768
- }
769
- .modern-chat-layout .chat-message.chat-assistant {
770
- align-self: flex-start;
771
- }
772
- .modern-chat-layout .chat-avatar {
773
- width: 2rem;
774
- height: 2rem;
775
- border-radius: 50%;
776
- display: flex;
777
- align-items: center;
778
- justify-content: center;
779
- flex-shrink: 0;
780
- font-size: 0.85rem;
781
- background: var(--tblr-secondary-bg-subtle, var(--bs-secondary-bg-subtle, #e9ecef));
782
- color: var(--tblr-secondary-color, var(--bs-secondary-color, #6c757d));
783
- }
784
- .modern-chat-layout .chat-user .chat-avatar {
785
- background: #0d6efd;
786
- color: #fff;
787
- }
788
- .modern-chat-layout .chat-bubble {
789
- padding: 0.6rem 1rem;
790
- border-radius: 1rem;
791
- line-height: 1.5;
792
- word-wrap: break-word;
793
- overflow-wrap: break-word;
794
- }
795
- .modern-chat-layout .chat-user .chat-bubble {
796
- background: #0d6efd;
797
- color: #fff;
798
- border-bottom-right-radius: 0.25rem;
799
- }
800
- .modern-chat-layout .chat-assistant .chat-bubble {
801
- background: var(--tblr-secondary-bg-subtle, var(--bs-secondary-bg-subtle, #f0f2f5));
802
- color: var(--tblr-body-color, var(--bs-body-color, #212529));
803
- border-bottom-left-radius: 0.25rem;
804
- }
805
- /* Markdown content inside bubbles */
806
- .modern-chat-layout .chat-bubble h1,
807
- .modern-chat-layout .chat-bubble h2,
808
- .modern-chat-layout .chat-bubble h3,
809
- .modern-chat-layout .chat-bubble h4 {
810
- margin-top: 0.5rem;
811
- margin-bottom: 0.25rem;
812
- }
813
- .modern-chat-layout .chat-bubble h1 { font-size: 1.3rem; }
814
- .modern-chat-layout .chat-bubble h2 { font-size: 1.15rem; }
815
- .modern-chat-layout .chat-bubble h3 { font-size: 1.05rem; }
816
- .modern-chat-layout .chat-bubble h4 { font-size: 1rem; }
817
- .modern-chat-layout .chat-bubble p {
818
- margin-bottom: 0.4rem;
819
- }
820
- .modern-chat-layout .chat-bubble p:last-child {
821
- margin-bottom: 0;
822
- }
823
- .modern-chat-layout .chat-bubble ul,
824
- .modern-chat-layout .chat-bubble ol {
825
- padding-left: 1.5rem;
826
- margin-bottom: 0.4rem;
827
- }
828
- .modern-chat-layout .chat-bubble table {
829
- width: 100%;
830
- border-collapse: collapse;
831
- margin: 0.5rem 0;
832
- font-size: 0.9em;
833
- }
834
- .modern-chat-layout .chat-bubble table th,
835
- .modern-chat-layout .chat-bubble table td {
836
- border: 1px solid rgba(0,0,0,0.15);
837
- padding: 0.3rem 0.5rem;
838
- }
839
- .modern-chat-layout .chat-bubble table th {
840
- background: rgba(0,0,0,0.05);
841
- font-weight: 600;
842
- }
843
- .modern-chat-layout .chat-bubble pre {
844
- background: rgba(0,0,0,0.06);
845
- padding: 0.5rem;
846
- border-radius: 0.5rem;
847
- overflow-x: auto;
848
- margin: 0.4rem 0;
849
- }
850
- .modern-chat-layout .chat-bubble code {
851
- font-size: 0.88em;
852
- }
853
- .modern-chat-layout .chat-bubble p > code {
854
- background: rgba(0,0,0,0.06);
855
- padding: 0.1rem 0.3rem;
856
- border-radius: 0.25rem;
857
- }
858
- .modern-chat-layout .chat-user .chat-bubble pre {
859
- background: rgba(255,255,255,0.15);
860
- }
861
- .modern-chat-layout .chat-user .chat-bubble p > code {
862
- background: rgba(255,255,255,0.15);
863
- }
864
- .modern-chat-layout .chat-user .chat-bubble table th,
865
- .modern-chat-layout .chat-user .chat-bubble table td {
866
- border-color: rgba(255,255,255,0.25);
867
- }
868
- .modern-chat-layout .chat-user .chat-bubble table th {
869
- background: rgba(255,255,255,0.1);
870
- }
871
- /* Skill attribution badge */
872
- .modern-chat-layout .chat-bubble .badge.bg-info {
873
- display: inline-block;
874
- margin-bottom: 6px;
875
- font-size: 0.7rem;
876
- font-weight: 600;
877
- letter-spacing: 0.3px;
878
- text-transform: uppercase;
879
- opacity: 0.85;
880
- }
881
- .modern-chat-layout .chat-bubble .card.bg-secondary-subtle {
882
- border: none;
883
- background-color: rgba(0,0,0,0.03) !important;
884
- margin-bottom: 0.5rem;
885
- }
886
- /* Input area for modern chat */
887
- .modern-chat-layout .copilot-entry {
888
- border-top: 1px solid var(--tblr-border-color, var(--bs-border-color, #dee2e6));
889
- padding-top: 0.75rem;
890
- margin-top: 0.5rem;
891
- }
892
- .modern-chat-layout .copilot-entry textarea {
893
- border-radius: 1.5rem;
894
- padding: 0.6rem 1rem;
895
- resize: none;
896
- }
897
- /* Streaming scratch in modern chat */
898
- .modern-chat-layout .next_response_scratch {
899
- padding: 0 1rem;
900
- }
901
- .modern-chat-layout .next_response_scratch:not(:empty) {
902
- margin-bottom: 0.5rem;
903
- }
904
- /* Interaction segment (tool cards) inside modern chat */
905
- .modern-chat-layout .interaction-segment {
906
- border-top: none;
907
- }
908
- /* Modern Sessions Sidebar */
909
- .modern-sessions-header {
910
- display: flex;
911
- align-items: center;
912
- justify-content: space-between;
913
- padding: 0.6rem 0.75rem;
914
- margin-bottom: 0.75rem;
915
- background: var(--tblr-secondary-bg-subtle, var(--bs-secondary-bg-subtle, #f8f9fa));
916
- border-radius: 0.75rem;
917
- border-bottom: 1px solid var(--tblr-border-color, var(--bs-border-color, #dee2e6));
918
- position: sticky;
919
- top: 0;
920
- z-index: 1;
921
- }
922
- .modern-sessions .modern-session-item {
923
- border-radius: 0.75rem;
924
- padding: 0.65rem 0.75rem;
925
- margin-bottom: 0.4rem;
926
- border: 1px solid var(--tblr-border-color, var(--bs-border-color, #dee2e6));
927
- cursor: pointer;
928
- transition: all 0.15s ease;
929
- }
930
- .modern-sessions .modern-session-item:hover {
931
- box-shadow: 0 2px 8px rgba(0,0,0,0.07);
932
- background-color: var(--tblr-secondary-bg-subtle, var(--bs-secondary-bg-subtle, #f8f9fa));
933
- }
934
- .modern-sessions .modern-session-item.active-session {
935
- border-left: 3px solid #0d6efd;
936
- background-color: rgba(13, 110, 253, 0.05);
937
- }
938
- .modern-sessions .modern-session-item i.fa-trash-alt {
939
- display: none;
940
- font-size: 0.8em;
941
- }
942
- .modern-sessions .modern-session-item:hover i.fa-trash-alt {
943
- display: inline;
944
- }
945
- .modern-sessions .modern-session-item .prevrun_content {
946
- font-size: 0.85em;
947
- color: var(--tblr-secondary-color, var(--bs-secondary-color, #6c757d));
948
- white-space: nowrap;
949
- overflow: hidden;
950
- text-overflow: ellipsis;
951
- }`,
952
- ),
669
+ hasInputForm && input_form,
670
+ style(agents_css),
953
671
  script(domReady(`$( "#inputuserinput" ).autogrow({paddingBottom: 20});`)),
954
672
  script(
955
673
  `
@@ -1154,7 +872,37 @@ const run = async (
1154
872
  : '<div class="agent-waiting-indicator"><div class="typing-dots"><span></span><span></span><span></span></div></div>';
1155
873
  $('div.next_response_scratch').before(indicator);
1156
874
  scrollAgentToBottom();
1157
- };`,
875
+ };
876
+ document.addEventListener('click', async (e) => {
877
+ const target = e.target.closest('.copy-to-clipboard-elem');
878
+ if (!target) return;
879
+
880
+ // Check if the click was in the top-right corner where the icon is
881
+ const rect = target.getBoundingClientRect();
882
+ const clickX = e.clientX - rect.left;
883
+ const clickY = e.clientY - rect.top;
884
+
885
+ // Icon is at top: 4px, right: 4px, ~16px size — give a generous hit area
886
+ const iconHitArea = 24;
887
+ const isIconClick =
888
+ clickX >= rect.width - iconHitArea &&
889
+ clickX <= rect.width &&
890
+ clickY >= 0 &&
891
+ clickY <= iconHitArea;
892
+
893
+ if (!isIconClick) return;
894
+
895
+ e.stopPropagation();
896
+ e.preventDefault();
897
+
898
+ try {
899
+ await navigator.clipboard.writeText(target.innerText);
900
+ target.classList.add('copy-success');
901
+ setTimeout(() => target.classList.remove('copy-success'), 1000);
902
+ } catch (err) {
903
+ console.error('Failed to copy:', err);
904
+ }
905
+ });`,
1158
906
  stream &&
1159
907
  domReady(
1160
908
  `$('form.agent-view input[name=page_load_tag]').val(window._sc_pageloadtag)`,
package/agents.css ADDED
@@ -0,0 +1,401 @@
1
+ div.interaction-segment:not(:first-child) {
2
+ border-top: 1px solid #e7e7e7;
3
+ }
4
+ div.interaction-segment {
5
+ padding-top: 5px;
6
+ padding-bottom: 5px;
7
+ }
8
+ div.interaction-segment p {
9
+ margin-bottom: 0px;
10
+ }
11
+ div.interaction-segment div.card {
12
+ margin-top: 0.5rem;
13
+ }
14
+ div.interaction-segment.to-right {
15
+ display: flex;
16
+ flex-direction: row-reverse;
17
+ }
18
+ div.interaction-segment.to-right div.badgewrap {
19
+ display: flex;
20
+ flex-direction: row-reverse;
21
+ }
22
+ div.prevcopilotrun:hover {
23
+ cursor: pointer;
24
+ background-color: var(
25
+ --tblr-secondary-bg-subtle,
26
+ var(--bs-secondary-bg-subtle, gray)
27
+ );
28
+ }
29
+ div.prevcopilotrun i.fa-trash-alt {
30
+ display: none;
31
+ }
32
+ div.prevcopilotrun:hover i.fa-trash-alt {
33
+ display: block;
34
+ }
35
+ .copilot-entry .submit-button:hover {
36
+ cursor: pointer;
37
+ }
38
+ .copilot-entry span.attach_agent_image_wrap i:hover {
39
+ cursor: pointer;
40
+ }
41
+
42
+ .copilot-entry .submit-button {
43
+ position: relative;
44
+ top: -1.8rem;
45
+ left: 0.1rem;
46
+ }
47
+ .copilot-entry #audioinputicon {
48
+ position: relative;
49
+ top: -1.8rem;
50
+ right: 0.7rem;
51
+ cursor: pointer;
52
+ float: right;
53
+ }
54
+ .copilot-entry .debugicon {
55
+ position: relative;
56
+ top: -1.8rem;
57
+ left: 0.1rem;
58
+ cursor: pointer;
59
+ }
60
+ .copilot-entry .cancelbtn {
61
+ position: relative;
62
+ top: -1.8rem;
63
+ left: 0.1rem;
64
+ cursor: pointer;
65
+ }
66
+ .copilot-entry .skill-form-widget {
67
+ position: relative;
68
+ top: -2rem;
69
+ left: 0.4rem;
70
+ display: inline;
71
+ }
72
+ .session-open-sessions,
73
+ .open-prev-runs {
74
+ cursor: pointer;
75
+ }
76
+ .copilot-entry span.attach_agent_image_wrap {
77
+ position: relative;
78
+ top: -1.8rem;
79
+ left: 0.2rem;
80
+ }
81
+ .copilot-entry.dragover {
82
+ outline: 2px dashed var(--tblr-primary, #0054a6);
83
+ outline-offset: -2px;
84
+ background: var(--tblr-primary-bg-subtle, rgba(0, 84, 166, 0.05));
85
+ border-radius: 0.25rem;
86
+ }
87
+ .copilot-entry .explainer {
88
+ position: relative;
89
+ top: -1.2rem;
90
+ display: block;
91
+ }
92
+ .col-0 {
93
+ width: 0%;
94
+ }
95
+ .copilot-entry {
96
+ margin-bottom: -1.25rem;
97
+ margin-top: 1rem;
98
+ }
99
+ p.prevrun_content {
100
+ white-space: nowrap;
101
+ overflow: hidden;
102
+ margin-bottom: 0px;
103
+ display: block;
104
+ text-overflow: ellipsis;
105
+ }
106
+ /* Typing / Waiting Indicator */
107
+ .agent-waiting-indicator {
108
+ display: flex;
109
+ align-items: center;
110
+ padding: 0.75rem 1rem;
111
+ }
112
+ .typing-dots {
113
+ display: flex;
114
+ gap: 4px;
115
+ align-items: center;
116
+ }
117
+ .typing-dots span {
118
+ width: 8px;
119
+ height: 8px;
120
+ border-radius: 50%;
121
+ background: #6c757d;
122
+ animation: typingBounce 1.4s infinite ease-in-out both;
123
+ }
124
+ .typing-dots span:nth-child(1) {
125
+ animation-delay: -0.32s;
126
+ }
127
+ .typing-dots span:nth-child(2) {
128
+ animation-delay: -0.16s;
129
+ }
130
+ .typing-dots span:nth-child(3) {
131
+ animation-delay: 0s;
132
+ }
133
+ @keyframes typingBounce {
134
+ 0%,
135
+ 80%,
136
+ 100% {
137
+ transform: scale(0.6);
138
+ opacity: 0.4;
139
+ }
140
+ 40% {
141
+ transform: scale(1);
142
+ opacity: 1;
143
+ }
144
+ }
145
+ /* Modern Chat Layout */
146
+ .modern-chat-layout {
147
+ display: flex;
148
+ flex-direction: column;
149
+ height: 100%;
150
+ }
151
+ .modern-chat-layout #copilotinteractions {
152
+ /*max-height: 70vh;*/
153
+ overflow-y: auto;
154
+ padding: 1rem;
155
+ display: flex;
156
+ flex-direction: column;
157
+ gap: 0.75rem;
158
+ }
159
+ .modern-chat-layout .chat-message {
160
+ display: flex;
161
+ gap: 0.5rem;
162
+ max-width: 85%;
163
+ align-items: flex-start;
164
+ }
165
+ .modern-chat-layout .chat-message.chat-user {
166
+ align-self: flex-end;
167
+ flex-direction: row-reverse;
168
+ }
169
+ .modern-chat-layout .chat-message.chat-assistant {
170
+ align-self: flex-start;
171
+ }
172
+ .modern-chat-layout .chat-avatar {
173
+ width: 2rem;
174
+ height: 2rem;
175
+ border-radius: 50%;
176
+ display: flex;
177
+ align-items: center;
178
+ justify-content: center;
179
+ flex-shrink: 0;
180
+ font-size: 0.85rem;
181
+ background: var(
182
+ --tblr-secondary-bg-subtle,
183
+ var(--bs-secondary-bg-subtle, #e9ecef)
184
+ );
185
+ color: var(--tblr-secondary-color, var(--bs-secondary-color, #6c757d));
186
+ }
187
+ .modern-chat-layout .chat-user .chat-avatar {
188
+ background: #0d6efd;
189
+ color: #fff;
190
+ }
191
+ .modern-chat-layout .chat-bubble {
192
+ padding: 0.6rem 1rem;
193
+ border-radius: 1rem;
194
+ line-height: 1.5;
195
+ word-wrap: break-word;
196
+ overflow-wrap: break-word;
197
+ }
198
+ .modern-chat-layout .chat-user .chat-bubble {
199
+ background: #0d6efd;
200
+ color: #fff;
201
+ border-bottom-right-radius: 0.25rem;
202
+ }
203
+ .modern-chat-layout .chat-assistant .chat-bubble {
204
+ background: var(
205
+ --tblr-secondary-bg-subtle,
206
+ var(--bs-secondary-bg-subtle, #f0f2f5)
207
+ );
208
+ color: var(--tblr-body-color, var(--bs-body-color, #212529));
209
+ border-bottom-left-radius: 0.25rem;
210
+ }
211
+ /* Markdown content inside bubbles */
212
+ .modern-chat-layout .chat-bubble h1,
213
+ .modern-chat-layout .chat-bubble h2,
214
+ .modern-chat-layout .chat-bubble h3,
215
+ .modern-chat-layout .chat-bubble h4 {
216
+ margin-top: 0.5rem;
217
+ margin-bottom: 0.25rem;
218
+ }
219
+ .modern-chat-layout .chat-bubble h1 {
220
+ font-size: 1.3rem;
221
+ }
222
+ .modern-chat-layout .chat-bubble h2 {
223
+ font-size: 1.15rem;
224
+ }
225
+ .modern-chat-layout .chat-bubble h3 {
226
+ font-size: 1.05rem;
227
+ }
228
+ .modern-chat-layout .chat-bubble h4 {
229
+ font-size: 1rem;
230
+ }
231
+ .modern-chat-layout .chat-bubble p {
232
+ margin-bottom: 0.4rem;
233
+ }
234
+ .modern-chat-layout .chat-bubble p:last-child {
235
+ margin-bottom: 0;
236
+ }
237
+ .modern-chat-layout .chat-bubble ul,
238
+ .modern-chat-layout .chat-bubble ol {
239
+ padding-left: 1.5rem;
240
+ margin-bottom: 0.4rem;
241
+ }
242
+ .modern-chat-layout .chat-bubble table {
243
+ width: 100%;
244
+ border-collapse: collapse;
245
+ margin: 0.5rem 0;
246
+ font-size: 0.9em;
247
+ }
248
+ .modern-chat-layout .chat-bubble table th,
249
+ .modern-chat-layout .chat-bubble table td {
250
+ border: 1px solid rgba(0, 0, 0, 0.15);
251
+ padding: 0.3rem 0.5rem;
252
+ }
253
+ .modern-chat-layout .chat-bubble table th {
254
+ background: rgba(0, 0, 0, 0.05);
255
+ font-weight: 600;
256
+ }
257
+ .modern-chat-layout .chat-bubble pre {
258
+ background: rgba(0, 0, 0, 0.06);
259
+ padding: 0.5rem;
260
+ border-radius: 0.5rem;
261
+ overflow-x: auto;
262
+ margin: 0.4rem 0;
263
+ }
264
+ .modern-chat-layout .chat-bubble code {
265
+ font-size: 0.88em;
266
+ }
267
+ .modern-chat-layout .chat-bubble p > code {
268
+ background: rgba(0, 0, 0, 0.06);
269
+ padding: 0.1rem 0.3rem;
270
+ border-radius: 0.25rem;
271
+ }
272
+ .modern-chat-layout .chat-user .chat-bubble pre {
273
+ background: rgba(255, 255, 255, 0.15);
274
+ }
275
+ .modern-chat-layout .chat-user .chat-bubble p > code {
276
+ background: rgba(255, 255, 255, 0.15);
277
+ }
278
+ .modern-chat-layout .chat-user .chat-bubble table th,
279
+ .modern-chat-layout .chat-user .chat-bubble table td {
280
+ border-color: rgba(255, 255, 255, 0.25);
281
+ }
282
+ .modern-chat-layout .chat-user .chat-bubble table th {
283
+ background: rgba(255, 255, 255, 0.1);
284
+ }
285
+ /* Skill attribution badge */
286
+ .modern-chat-layout .chat-bubble .badge.bg-info {
287
+ display: inline-block;
288
+ margin-bottom: 6px;
289
+ font-size: 0.7rem;
290
+ font-weight: 600;
291
+ letter-spacing: 0.3px;
292
+ text-transform: uppercase;
293
+ opacity: 0.85;
294
+ }
295
+ .modern-chat-layout .chat-bubble .card.bg-secondary-subtle {
296
+ border: none;
297
+ background-color: rgba(0, 0, 0, 0.03) !important;
298
+ margin-bottom: 0.5rem;
299
+ }
300
+ /* Input area for modern chat */
301
+ .modern-chat-layout .copilot-entry {
302
+ border-top: 1px solid
303
+ var(--tblr-border-color, var(--bs-border-color, #dee2e6));
304
+ padding-top: 0.75rem;
305
+ margin-top: 0.5rem;
306
+ }
307
+ .modern-chat-layout .copilot-entry textarea {
308
+ border-radius: 1.5rem;
309
+ padding: 0.6rem 1rem;
310
+ resize: none;
311
+ }
312
+ /* Streaming scratch in modern chat */
313
+ .modern-chat-layout .next_response_scratch {
314
+ padding: 0 1rem;
315
+ }
316
+ .modern-chat-layout .next_response_scratch:not(:empty) {
317
+ margin-bottom: 0.5rem;
318
+ }
319
+ /* Interaction segment (tool cards) inside modern chat */
320
+ .modern-chat-layout .interaction-segment {
321
+ border-top: none;
322
+ }
323
+ /* Modern Sessions Sidebar */
324
+ .modern-sessions-header {
325
+ display: flex;
326
+ align-items: center;
327
+ justify-content: space-between;
328
+ padding: 0.6rem 0.75rem;
329
+ margin-bottom: 0.75rem;
330
+ background: var(
331
+ --tblr-secondary-bg-subtle,
332
+ var(--bs-secondary-bg-subtle, #f8f9fa)
333
+ );
334
+ border-radius: 0.75rem;
335
+ border-bottom: 1px solid
336
+ var(--tblr-border-color, var(--bs-border-color, #dee2e6));
337
+ position: sticky;
338
+ top: 0;
339
+ z-index: 1;
340
+ }
341
+ .modern-sessions .modern-session-item {
342
+ border-radius: 0.75rem;
343
+ padding: 0.65rem 0.75rem;
344
+ margin-bottom: 0.4rem;
345
+ border: 1px solid var(--tblr-border-color, var(--bs-border-color, #dee2e6));
346
+ cursor: pointer;
347
+ transition: all 0.15s ease;
348
+ }
349
+ .modern-sessions .modern-session-item:hover {
350
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07);
351
+ background-color: var(
352
+ --tblr-secondary-bg-subtle,
353
+ var(--bs-secondary-bg-subtle, #f8f9fa)
354
+ );
355
+ }
356
+ .modern-sessions .modern-session-item.active-session {
357
+ border-left: 3px solid #0d6efd;
358
+ background-color: rgba(13, 110, 253, 0.05);
359
+ }
360
+ .modern-sessions .modern-session-item i.fa-trash-alt {
361
+ display: none;
362
+ font-size: 0.8em;
363
+ }
364
+ .modern-sessions .modern-session-item:hover i.fa-trash-alt {
365
+ display: inline;
366
+ }
367
+ .modern-sessions .modern-session-item .prevrun_content {
368
+ font-size: 0.85em;
369
+ color: var(--tblr-secondary-color, var(--bs-secondary-color, #6c757d));
370
+ white-space: nowrap;
371
+ overflow: hidden;
372
+ text-overflow: ellipsis;
373
+ }
374
+ .copy-to-clipboard-elem {
375
+ position: relative;
376
+ }
377
+
378
+ .copy-to-clipboard-elem::before {
379
+ content: "📋";
380
+ position: absolute;
381
+ top: 4px;
382
+ right: 4px;
383
+ font-size: 16px;
384
+ line-height: 1;
385
+ cursor: pointer;
386
+ opacity: 0;
387
+ pointer-events: none;
388
+ transition: opacity 0.15s ease-in-out;
389
+ z-index: 10;
390
+ user-select: none;
391
+ }
392
+
393
+ .copy-to-clipboard-elem:hover::before {
394
+ opacity: 1;
395
+ pointer-events: auto;
396
+ }
397
+
398
+ .copy-to-clipboard-elem.copy-success::before {
399
+ content: "✓";
400
+ color: green;
401
+ }
package/common.js CHANGED
@@ -43,6 +43,7 @@ const get_skills = () => {
43
43
  require("./skills/Subagent"),
44
44
  require("./skills/ExternalSkill"),
45
45
  require("./skills/PlanApproval"),
46
+ require("./skills/LongTermMemory"),
46
47
  //require("./skills/AdaptiveFeedback"),
47
48
  ...exchange_skills,
48
49
  ];
@@ -57,8 +58,10 @@ const get_skill_instances = (config) => {
57
58
  const instances = [];
58
59
  for (const skillCfg of config.skills) {
59
60
  const klass = get_skill_class(skillCfg.skill_type);
60
- const skill = new klass(skillCfg);
61
- instances.push(skill);
61
+ if (klass) {
62
+ const skill = new klass(skillCfg);
63
+ instances.push(skill);
64
+ }
62
65
  }
63
66
  return instances;
64
67
  };
@@ -220,7 +223,7 @@ const wrapSegment = (html, who, to_right, layout, user) =>
220
223
  : layout && layout.startsWith("Modern chat")
221
224
  ? `<div class="chat-message ${to_right ? "chat-user" : "chat-assistant"}">` +
222
225
  `<div class="chat-avatar"${user ? ` title="${user.email} at ${new Date().toString()}"` : ""}><i class="fas ${to_right ? "fa-user" : "fa-robot"}"></i></div>` +
223
- `<div class="chat-bubble">${html}</div>` +
226
+ `<div class="chat-bubble${" copy-to-clipboard-elem"}">${html}</div>` +
224
227
  `</div>`
225
228
  : `<div class="interaction-segment ${to_right ? "to-right" : ""}"><div><div class="badgewrap"><span class="badge bg-secondary">` +
226
229
  who +
@@ -237,6 +240,10 @@ const wrapCard = (title, ...inners) =>
237
240
 
238
241
  const is_debug_mode = (config, user) => user?.role_id === 1;
239
242
 
243
+ function extractText(html) {
244
+ return html.replace(/<[^>]*>/g, '');
245
+ }
246
+
240
247
  const process_interaction = async (
241
248
  run,
242
249
  config,
@@ -414,6 +421,7 @@ const process_interaction = async (
414
421
  myHasResult = true;
415
422
  let result = await tool.tool.process(tool_call.input, {
416
423
  req,
424
+ run,
417
425
  });
418
426
  const tool_response = result.add_response || result;
419
427
  toolResults[tool_call.tool_call_id] = result;
@@ -744,4 +752,5 @@ module.exports = {
744
752
  is_debug_mode,
745
753
  get_initial_interactions,
746
754
  nubBy,
755
+ extractText
747
756
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/agents",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "AI agents for Saltcorn",
5
5
  "main": "index.js",
6
6
  "dependencies": {
package/skills/Fetch.js CHANGED
@@ -15,9 +15,69 @@ const { features } = require("@saltcorn/data/db/state");
15
15
  const { button } = require("@saltcorn/markup/tags");
16
16
  const { validID } = require("@saltcorn/markup/layout_utils");
17
17
 
18
- const vm = require("vm");
18
+ class CookieJar {
19
+ constructor(source) {
20
+ this.cookies = new Map();
19
21
 
20
- //const { fieldProperties } = require("./helpers");
22
+ if (source instanceof CookieJar) {
23
+ // Copy from another CookieJar instance
24
+ for (const [name, value] of source.cookies) {
25
+ this.cookies.set(name, value);
26
+ }
27
+ } else if (source && typeof source === "object") {
28
+ // Hydrate from a plain object (e.g. output of toObject())
29
+ for (const [name, value] of Object.entries(source)) {
30
+ this.cookies.set(name, String(value));
31
+ }
32
+ }
33
+ // If source is undefined/null, start empty
34
+ }
35
+
36
+ // Parse Set-Cookie headers from a response and store them
37
+ storeFromResponse(response) {
38
+ const setCookieHeaders = response.headers.getSetCookie
39
+ ? response.headers.getSetCookie()
40
+ : [response.headers.get("set-cookie")].filter(Boolean);
41
+
42
+ for (const header of setCookieHeaders) {
43
+ this._parseAndStore(header);
44
+ }
45
+ }
46
+
47
+ // Parse a single "name=value; Path=/; HttpOnly; ..." string
48
+ _parseAndStore(header) {
49
+ const [nameValue] = header.split(";");
50
+ const eqIdx = nameValue.indexOf("=");
51
+ if (eqIdx > 0) {
52
+ const name = nameValue.slice(0, eqIdx).trim();
53
+ const value = nameValue.slice(eqIdx + 1).trim();
54
+ this.cookies.set(name, value);
55
+ }
56
+ }
57
+
58
+ // Build a Cookie header string suitable for outgoing requests
59
+ toHeader() {
60
+ return Array.from(this.cookies.entries())
61
+ .map(([name, value]) => `${name}=${value}`)
62
+ .join("; ");
63
+ }
64
+
65
+ // Apply cookies to a headers object (mutates and returns it)
66
+ applyTo(headers = {}) {
67
+ if (this.cookies.size > 0) {
68
+ headers["Cookie"] = this.toHeader();
69
+ }
70
+ return headers;
71
+ }
72
+
73
+ get size() {
74
+ return this.cookies.size;
75
+ }
76
+
77
+ toObject() {
78
+ return Object.fromEntries(this.cookies);
79
+ }
80
+ }
21
81
 
22
82
  class FetchSkill {
23
83
  static skill_name = "Fetch";
@@ -31,7 +91,7 @@ class FetchSkill {
31
91
  }
32
92
 
33
93
  static async configFields() {
34
- return [];
94
+ return [{ name: "cookiejar", label: "Cookie Jar", type: "Bool" }];
35
95
  }
36
96
  systemPrompt() {
37
97
  return "If you need to retrieve the contents of a web page, use the fetch_web_page to make a GET request to a specified URL.";
@@ -40,8 +100,29 @@ class FetchSkill {
40
100
  provideTools = () => {
41
101
  return {
42
102
  type: "function",
43
- process: async (row) => {
44
- const resp = await fetch(row.url);
103
+ process: async (row, { run }) => {
104
+ const opts = { headers: {} };
105
+ const jar = new CookieJar(run.context.cookiejar || {});
106
+ if (this.cookiejar) {
107
+ opts.headers = jar.applyTo();
108
+ opts.credentials = "same-origin";
109
+ opts.redirect = "manual";
110
+ }
111
+
112
+ if (row.method) opts.method = row.method;
113
+ if (row.content_type) opts.headers["Content-Type"] = row.content_type;
114
+ if (row.body) opts.body = row.body;
115
+
116
+ let resp = await fetch(row.url, opts);
117
+ if (resp.status == 302 && (!row.method || row.method === "GET")) {
118
+ if (this.cookiejar) jar.storeFromResponse(resp);
119
+ resp = await fetch(resp.headers.get("location"), opts);
120
+ }
121
+
122
+ if (this.cookiejar) {
123
+ jar.storeFromResponse(resp);
124
+ run.context.cookiejar = jar.toObject();
125
+ }
45
126
  return await resp.text();
46
127
  },
47
128
  function: {
@@ -55,6 +136,20 @@ class FetchSkill {
55
136
  description: "The URL to fetch with HTTP",
56
137
  type: "string",
57
138
  },
139
+ method: {
140
+ description: "The HTTP method",
141
+ type: "string",
142
+ enum: ["GET", "POST", "PUT", "DELETE"],
143
+ },
144
+ body: {
145
+ description: "The request body as a string (POST and PUT only)",
146
+ type: "string",
147
+ },
148
+ content_type: {
149
+ description:
150
+ "The request body content type, e.g. application/x-www-form-urlencoded or application/json (POST and PUT only)",
151
+ type: "string",
152
+ },
58
153
  },
59
154
  },
60
155
  },
@@ -0,0 +1,229 @@
1
+ const { div, pre } = require("@saltcorn/markup/tags");
2
+ const Workflow = require("@saltcorn/data/models/workflow");
3
+ const Form = require("@saltcorn/data/models/form");
4
+ const Table = require("@saltcorn/data/models/table");
5
+ const Field = require("@saltcorn/data/models/field");
6
+ const View = require("@saltcorn/data/models/view");
7
+ const { getState } = require("@saltcorn/data/db/state");
8
+ const db = require("@saltcorn/data/db");
9
+ const { interpolate } = require("@saltcorn/data/utils");
10
+ const { nubBy } = require("../common");
11
+
12
+ class LongTermMemory {
13
+ static skill_name = "Memory";
14
+
15
+ get skill_label() {
16
+ return `Memory`;
17
+ }
18
+
19
+ constructor(cfg) {
20
+ Object.assign(this, cfg);
21
+ }
22
+
23
+ systemPrompt() {
24
+ return `You have access to a memory bank you can read from or write to. You should search the memory bank with the search_memory tool with any search terms that might be relevant to the user's query or the result of a tool call. When you learn something noteworthy (from the user or from the result of a tool call) store it in memory with the store_in_memory tool. Mark it as personal if it is only true or relevant for the specific user. Don't tell the user when you are storing to and retrieving from memory. ${
25
+ this.add_sys_prompt
26
+ ? ` Additional instructions for the memory tools: ${this.add_sys_prompt}`
27
+ : ""
28
+ }`;
29
+ }
30
+
31
+ static async configFields() {
32
+ return [
33
+ {
34
+ name: "add_sys_prompt",
35
+ label: "Additional prompt",
36
+ type: "String",
37
+ fieldview: "textarea",
38
+ },
39
+ ];
40
+ }
41
+
42
+ async get_table() {
43
+ const table0 = Table.findOne("AgentLongTermMemory");
44
+ if (table0) return table0;
45
+ const tables = await Table.find({ name: "AgentLongTermMemory" });
46
+ if (tables.length) return tables[0];
47
+
48
+ //does not exist, create it
49
+ const table = await Table.create("AgentLongTermMemory", {});
50
+ await getState().refresh_tables(true);
51
+ await Field.create({
52
+ table,
53
+ name: "run_id",
54
+ label: "Run ID",
55
+ type: "Integer",
56
+ });
57
+ const uid_field = await Field.create({
58
+ table,
59
+ name: "user_id",
60
+ label: "User ID",
61
+ type: "Key to users",
62
+ attributes: {
63
+ on_delete: "Set null",
64
+ include_fts: false,
65
+ summary_field: "email",
66
+ },
67
+ });
68
+ await Field.create({
69
+ table,
70
+ name: "written_at",
71
+ label: "Written at",
72
+ type: "Date",
73
+ });
74
+ await Field.create({
75
+ table,
76
+ name: "agent_trigger_id",
77
+ label: "Agent trigger ID",
78
+ type: "Integer",
79
+ });
80
+ await Field.create({
81
+ table,
82
+ name: "memory_type",
83
+ label: "Memory type",
84
+ type: "String",
85
+ });
86
+ await Field.create({
87
+ table,
88
+ name: "personal",
89
+ label: "Personal",
90
+ type: "Bool",
91
+ });
92
+ await Field.create({
93
+ table,
94
+ name: "topic",
95
+ label: "Topic",
96
+ type: "String",
97
+ });
98
+ await Field.create({
99
+ table,
100
+ name: "contents",
101
+ label: "Contents",
102
+ type: "String",
103
+ });
104
+ await table.update({ ownership_field_id: uid_field.id });
105
+ await getState().refresh_tables();
106
+ return Table.findOne("AgentLongTermMemory");
107
+ }
108
+
109
+ provideTools() {
110
+ return [
111
+ {
112
+ type: "function",
113
+ function: {
114
+ name: "store_in_memory",
115
+ description: `Store a fact or observation in long-term memory`,
116
+ parameters: {
117
+ type: "object",
118
+ required: ["contents"],
119
+ properties: {
120
+ contents: {
121
+ type: "string",
122
+ description: "The contents of the fact or observations",
123
+ },
124
+ personal: {
125
+ type: "boolean",
126
+ description:
127
+ "Is this a fact or observation specifically about the person interacting with you now, which may not be true or relevant for another person",
128
+ },
129
+ },
130
+ },
131
+ },
132
+ process: async (arg, { req, run }) => {
133
+ const table = await this.get_table();
134
+ await table.insertRow({
135
+ run_id: run.id,
136
+ user_id: req.user?.id,
137
+ written_at: new Date(),
138
+ agent_trigger_id: run.trigger_id,
139
+ memory_type: "Episodic",
140
+ contents: arg.contents,
141
+ personal: arg.personal,
142
+ });
143
+ return "Recorded";
144
+ },
145
+ },
146
+ {
147
+ type: "function",
148
+ process: async (arg, { req }) => {
149
+ const table = await this.get_table();
150
+
151
+ const scState = getState();
152
+ const language = scState.pg_ts_config;
153
+ const use_websearch = scState.getConfig(
154
+ "search_use_websearch",
155
+ false,
156
+ );
157
+ let rows = [];
158
+ const user_id = req.user?.id;
159
+ const phrases =
160
+ typeof arg.phrases === "string" ? [arg.phrases] : arg.phrases;
161
+
162
+ if (use_websearch)
163
+ rows = await table.getRows({
164
+ _fts: {
165
+ fields: table.fields,
166
+ searchTerm: phrases.join(" OR "),
167
+ language,
168
+ use_websearch,
169
+ table: table.name,
170
+ schema: db.isSQLite ? undefined : db.getTenantSchema(),
171
+ },
172
+ ...(user_id
173
+ ? { or: [{ personal: false }, { personal: true, user_id }] }
174
+ : [{ personal: false }]),
175
+ });
176
+ else
177
+ for (const phrase of phrases) {
178
+ const my_rows = await table.getRows({
179
+ _fts: {
180
+ fields: table.fields,
181
+ searchTerm: phrase,
182
+ language,
183
+ use_websearch,
184
+ table: table.name,
185
+ schema: db.isSQLite ? undefined : db.getTenantSchema(),
186
+ },
187
+ ...(user_id
188
+ ? { or: [{ personal: false }, { personal: true, user_id }] }
189
+ : [{ personal: false }]),
190
+ });
191
+ rows.push(...my_rows);
192
+ }
193
+ const pk = table.pk_name;
194
+ rows = nubBy((r) => r[pk], rows);
195
+ //TODO sort most recent, only N memories
196
+ if (rows.length)
197
+ return (
198
+ "These memories were retrieved:\n\n" +
199
+ rows.map((r) => r.contents).join("\n")
200
+ );
201
+ else
202
+ return "There are no memories related to: " + phrases.join(" or ");
203
+ },
204
+ function: {
205
+ name: "search_memory",
206
+ description: `Search the memory bank by a search phrase`,
207
+ parameters: {
208
+ type: "object",
209
+ required: ["phrases"],
210
+ description:
211
+ "Search the memory bank by any of a number of phrases. This will return any memories that matches one or the other of the phrases",
212
+ properties: {
213
+ phrases: {
214
+ type: "array",
215
+ description:
216
+ "A phrase to search the memory bank with. The search phrase is the synatx used by web search engines: use double quotes for exact match, unquoted text for words in any order, dash (minus sign) to exclude a word. Do not use SQL or any other formal query language.",
217
+ items: {
218
+ type: "string",
219
+ },
220
+ },
221
+ },
222
+ },
223
+ },
224
+ },
225
+ ];
226
+ }
227
+ }
228
+
229
+ module.exports = LongTermMemory;