@perspective-dev/viewer 5.0.0 → 5.2.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.
- package/dist/cdn/perspective-viewer.js +9 -9
- package/dist/cdn/perspective-viewer.js.map +4 -4
- package/dist/css/botanical.css +1 -1
- package/dist/css/dracula.css +1 -1
- package/dist/css/gruvbox-dark.css +1 -1
- package/dist/css/gruvbox.css +1 -1
- package/dist/css/icons.css +1 -1
- package/dist/css/intl/de.css +1 -1
- package/dist/css/intl/es.css +1 -1
- package/dist/css/intl/fr.css +1 -1
- package/dist/css/intl/ja.css +1 -1
- package/dist/css/intl/pt.css +1 -1
- package/dist/css/intl/zh.css +1 -1
- package/dist/css/intl.css +1 -1
- package/dist/css/monokai.css +1 -1
- package/dist/css/phosphor.css +1 -1
- package/dist/css/pro-dark.css +1 -1
- package/dist/css/pro.css +1 -1
- package/dist/css/solarized-dark.css +1 -1
- package/dist/css/solarized.css +1 -1
- package/dist/css/themes.css +1 -1
- package/dist/css/vaporwave.css +1 -1
- package/dist/docs/perspective-docs.json +1 -0
- package/dist/esm/column-format.d.ts +19 -28
- package/dist/esm/perspective-viewer.d.ts +2 -0
- package/dist/esm/perspective-viewer.inline.js +8 -8
- package/dist/esm/perspective-viewer.inline.js.map +4 -4
- package/dist/esm/perspective-viewer.js +9 -9
- package/dist/esm/perspective-viewer.js.map +4 -4
- package/dist/esm/providers.d.ts +79 -0
- package/dist/esm/ts-rs/CompactDisplay.d.ts +5 -0
- package/dist/esm/ts-rs/CurrencyCode.d.ts +1 -0
- package/dist/esm/ts-rs/CurrencyDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/CurrencyNumberFormatStyle.d.ts +8 -0
- package/dist/esm/ts-rs/CurrencySign.d.ts +1 -0
- package/dist/esm/ts-rs/CustomDatetimeFormat.d.ts +1 -0
- package/dist/esm/ts-rs/CustomDatetimeStyleConfig.d.ts +54 -0
- package/dist/esm/ts-rs/CustomNumberFormatConfig.d.ts +34 -0
- package/dist/esm/ts-rs/DatetimeColorMode.d.ts +1 -0
- package/dist/esm/ts-rs/DatetimeFormatType.d.ts +6 -0
- package/dist/esm/ts-rs/FormatMode.d.ts +1 -0
- package/dist/esm/ts-rs/FormatUnit.d.ts +1 -0
- package/dist/esm/ts-rs/Notation.d.ts +17 -0
- package/dist/esm/ts-rs/NumberFormatStyle.d.ts +18 -0
- package/dist/esm/ts-rs/PanelViewerConfig.d.ts +43 -1
- package/dist/esm/ts-rs/PluginStaticConfig.d.ts +35 -0
- package/dist/esm/ts-rs/RestoreOptions.d.ts +19 -0
- package/dist/esm/ts-rs/RoundingMode.d.ts +1 -0
- package/dist/esm/ts-rs/RoundingPriority.d.ts +1 -0
- package/dist/esm/ts-rs/SignDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/SimpleDatetimeFormat.d.ts +1 -0
- package/dist/esm/ts-rs/SimpleDatetimeStyleConfig.d.ts +24 -0
- package/dist/esm/ts-rs/SplitRollupMode.d.ts +9 -0
- package/dist/esm/ts-rs/StringColorMode.d.ts +1 -0
- package/dist/esm/ts-rs/TrailingZeroDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/Unit.d.ts +1 -0
- package/dist/esm/ts-rs/UnitDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/UnitNumberFormatStyle.d.ts +6 -0
- package/dist/esm/ts-rs/UseGrouping.d.ts +1 -0
- package/dist/esm/ts-rs/ViewerConfig.d.ts +43 -1
- package/dist/esm/ts-rs/ViewerConfigInitial.d.ts +175 -0
- package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +72 -0
- package/dist/esm/ts-rs/WindowSort.d.ts +7 -0
- package/dist/esm/ts-rs/WindowSortDir.d.ts +6 -0
- package/dist/esm/ts-rs/WindowSpec.d.ts +47 -0
- package/dist/esm/ts-rs/Windows.d.ts +10 -0
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +7 -6
- package/dist/wasm/perspective-viewer.d.ts +222 -26
- package/dist/wasm/perspective-viewer.js +378 -125
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +18 -15
- package/package.json +2 -1
- package/src/css/_viewer_bundle.css +1 -0
- package/src/css/column-selector.css +9 -0
- package/src/css/column-settings-panel.css +67 -2
- package/src/css/column-style.css +4 -3
- package/src/css/config-selector.css +60 -4
- package/src/css/containers/context-menu.css +12 -6
- package/src/css/containers/tabs.css +2 -2
- package/src/css/form/code-editor.css +2 -3
- package/src/css/form/mirrored-textarea.css +81 -0
- package/src/css/viewer.css +336 -0
- package/src/rust/agent/client/mod.rs +19 -0
- package/src/rust/agent/client/protocol.rs +320 -0
- package/src/rust/agent/client/transport.rs +287 -0
- package/src/rust/agent/client/turn.rs +174 -0
- package/src/rust/agent/config.rs +159 -0
- package/src/rust/agent/docs.rs +293 -0
- package/src/rust/agent/markdown/block.rs +509 -0
- package/src/rust/agent/markdown/entity.rs +52 -0
- package/src/rust/agent/markdown/events.rs +71 -0
- package/src/rust/agent/markdown/inline.rs +497 -0
- package/src/rust/agent/markdown/mod.rs +466 -0
- package/src/rust/agent/mod.rs +23 -0
- package/src/rust/agent/preamble.md +16 -0
- package/src/rust/agent/runtime.rs +201 -0
- package/src/rust/agent/slot.rs +236 -0
- package/src/rust/agent/tools.rs +1110 -0
- package/src/rust/components/chat_panel.rs +289 -0
- package/src/rust/components/column_selector/active_column.rs +21 -27
- package/src/rust/components/column_selector/column_selector_column_row.rs +73 -0
- package/src/rust/components/column_selector/config_selector.rs +83 -8
- package/src/rust/components/column_selector/expr_edit_button.rs +7 -1
- package/src/rust/components/column_selector/inactive_column.rs +8 -1
- package/src/rust/components/column_selector.rs +27 -3
- package/src/rust/components/column_settings_sidebar/style_tab.rs +2 -1
- package/src/rust/components/column_settings_sidebar/window_tab.rs +34 -0
- package/src/rust/components/column_settings_sidebar.rs +163 -16
- package/src/rust/components/containers/dragdrop_list.rs +110 -3
- package/src/rust/components/containers/sidebar.rs +99 -22
- package/src/rust/components/containers/split_panel.rs +6 -0
- package/src/rust/components/editable_header.rs +2 -0
- package/src/rust/components/form/code_editor.rs +14 -23
- package/src/rust/components/form/debug.rs +8 -5
- package/src/rust/components/form/mirrored_textarea.rs +165 -0
- package/src/rust/components/form/mod.rs +1 -0
- package/src/rust/components/main_panel.rs +5 -0
- package/src/rust/components/mod.rs +3 -0
- package/src/rust/components/panel_menu.rs +7 -0
- package/src/rust/components/panel_tab.rs +5 -0
- package/src/rust/components/portal.rs +7 -0
- package/src/rust/components/settings_panel.rs +80 -2
- package/src/rust/components/viewer/msg.rs +2 -0
- package/src/rust/components/viewer/panels.rs +9 -14
- package/src/rust/components/viewer/render.rs +14 -0
- package/src/rust/components/viewer/settings.rs +33 -1
- package/src/rust/components/viewer.rs +2 -0
- package/src/rust/components/window_editor.rs +985 -0
- package/src/rust/config/datetime_column_style/custom.rs +17 -0
- package/src/rust/config/datetime_column_style/simple.rs +10 -0
- package/src/rust/config/number_string_format.rs +33 -0
- package/src/rust/config/options.rs +18 -0
- package/src/rust/config/plugin_static_config.rs +120 -5
- package/src/rust/config/viewer_config.rs +220 -1
- package/src/rust/config/workspace_config.rs +7 -6
- package/src/rust/custom_elements/viewer.rs +209 -18
- package/src/rust/custom_events.rs +29 -0
- package/src/rust/lib.rs +17 -1
- package/src/rust/presentation/column_locator.rs +16 -1
- package/src/rust/presentation.rs +8 -0
- package/src/rust/queries/columns_iter_set.rs +7 -1
- package/src/rust/queries/get_viewer_config.rs +5 -1
- package/src/rust/renderer/dispatch.rs +85 -58
- package/src/rust/renderer.rs +34 -1
- package/src/rust/session/column_defaults_update.rs +18 -0
- package/src/rust/session/drag_drop_update.rs +18 -0
- package/src/rust/session/metadata.rs +64 -9
- package/src/rust/session/replace_expression_update.rs +2 -0
- package/src/rust/session.rs +7 -5
- package/src/rust/tasks/create_panel.rs +18 -8
- package/src/rust/tasks/edit_window.rs +155 -0
- package/src/rust/tasks/mod.rs +2 -0
- package/src/rust/tasks/restore_panel.rs +34 -17
- package/src/rust/tasks/sync_update_panels.rs +7 -23
- package/src/rust/utils/browser/dragdrop.rs +12 -0
- package/src/rust/utils/browser/tests/debounce.rs +49 -0
- package/src/rust/utils/debounce.rs +76 -27
- package/src/rust/workspace.rs +112 -20
- package/src/svg/pin-icon.svg +5 -0
- package/src/themes/icons.css +1 -0
- package/src/themes/intl/de.css +23 -0
- package/src/themes/intl/es.css +23 -0
- package/src/themes/intl/fr.css +23 -0
- package/src/themes/intl/ja.css +23 -0
- package/src/themes/intl/pt.css +23 -0
- package/src/themes/intl/zh.css +23 -0
- package/src/themes/intl.css +26 -1
- package/src/ts/column-format.ts +67 -46
- package/src/ts/perspective-viewer.ts +2 -0
- package/src/ts/providers.ts +96 -0
- package/src/ts/ts-rs/CompactDisplay.ts +3 -0
- package/src/ts/ts-rs/CurrencyCode.ts +3 -0
- package/src/ts/ts-rs/CurrencyDisplay.ts +3 -0
- package/src/ts/ts-rs/CurrencyNumberFormatStyle.ts +6 -0
- package/src/ts/ts-rs/CurrencySign.ts +3 -0
- package/src/ts/ts-rs/CustomDatetimeFormat.ts +3 -0
- package/src/ts/ts-rs/CustomDatetimeStyleConfig.ts +54 -0
- package/src/ts/ts-rs/CustomNumberFormatConfig.ts +25 -0
- package/src/ts/ts-rs/DatetimeColorMode.ts +3 -0
- package/src/ts/ts-rs/DatetimeFormatType.ts +8 -0
- package/src/ts/ts-rs/FormatMode.ts +3 -0
- package/src/ts/ts-rs/FormatUnit.ts +3 -0
- package/src/ts/ts-rs/Notation.ts +11 -0
- package/src/ts/ts-rs/NumberFormatStyle.ts +12 -0
- package/src/ts/ts-rs/PanelViewerConfig.ts +48 -1
- package/src/ts/ts-rs/PluginStaticConfig.ts +36 -1
- package/src/ts/ts-rs/RestoreOptions.ts +20 -0
- package/src/ts/ts-rs/RoundingMode.ts +3 -0
- package/src/ts/ts-rs/RoundingPriority.ts +3 -0
- package/src/ts/ts-rs/SignDisplay.ts +3 -0
- package/src/ts/ts-rs/SimpleDatetimeFormat.ts +3 -0
- package/src/ts/ts-rs/SimpleDatetimeStyleConfig.ts +25 -0
- package/src/ts/ts-rs/SplitRollupMode.ts +11 -0
- package/src/ts/ts-rs/StringColorMode.ts +3 -0
- package/src/ts/ts-rs/TrailingZeroDisplay.ts +3 -0
- package/src/ts/ts-rs/Unit.ts +3 -0
- package/src/ts/ts-rs/UnitDisplay.ts +3 -0
- package/src/ts/ts-rs/UnitNumberFormatStyle.ts +5 -0
- package/src/ts/ts-rs/UseGrouping.ts +3 -0
- package/src/ts/ts-rs/ViewerConfig.ts +48 -1
- package/src/ts/ts-rs/ViewerConfigInitial.ts +164 -0
- package/src/ts/ts-rs/ViewerConfigUpdate.ts +81 -2
- package/src/ts/ts-rs/WindowSort.ts +9 -0
- package/src/ts/ts-rs/WindowSortDir.ts +8 -0
- package/src/ts/ts-rs/WindowSpec.ts +47 -0
- package/src/ts/ts-rs/Windows.ts +10 -0
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -6
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline4.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline5.js +0 -0
package/src/css/viewer.css
CHANGED
|
@@ -303,6 +303,14 @@
|
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
#modal_panel.pinned {
|
|
307
|
+
position: static;
|
|
308
|
+
flex: 0 0 auto;
|
|
309
|
+
width: auto;
|
|
310
|
+
pointer-events: all;
|
|
311
|
+
overflow: visible;
|
|
312
|
+
}
|
|
313
|
+
|
|
306
314
|
#modal_panel > .split-panel-divider {
|
|
307
315
|
border-left: 1px solid var(--psp-inactive--color, #6e6e6e);
|
|
308
316
|
margin-right: -5px;
|
|
@@ -656,6 +664,10 @@
|
|
|
656
664
|
content: var(--psp-label--debug-tab--content);
|
|
657
665
|
}
|
|
658
666
|
|
|
667
|
+
#chat_tabbar_tab:before {
|
|
668
|
+
content: var(--psp-label--chat-tab--content);
|
|
669
|
+
}
|
|
670
|
+
|
|
659
671
|
label#stack-label:before {
|
|
660
672
|
content: var(--psp-label--stack--content, "Stack");
|
|
661
673
|
}
|
|
@@ -664,3 +676,327 @@
|
|
|
664
676
|
content: var(--psp-label--alt-axis--content, "Alt Axis");
|
|
665
677
|
}
|
|
666
678
|
}
|
|
679
|
+
|
|
680
|
+
#chat_panel {
|
|
681
|
+
display: flex;
|
|
682
|
+
flex-direction: column;
|
|
683
|
+
flex: 1 1 auto;
|
|
684
|
+
min-height: 0;
|
|
685
|
+
min-width: 0;
|
|
686
|
+
contain: inline-size;
|
|
687
|
+
padding: 0px;
|
|
688
|
+
gap: 6px;
|
|
689
|
+
|
|
690
|
+
#chat_log:empty {
|
|
691
|
+
flex: 0 0 auto;
|
|
692
|
+
padding-bottom: 0px;
|
|
693
|
+
margin-bottom: -6px; /* offset gap */
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
#chat_log {
|
|
697
|
+
flex: 1 1 auto;
|
|
698
|
+
min-height: 0;
|
|
699
|
+
overflow-y: scroll;
|
|
700
|
+
display: flex;
|
|
701
|
+
flex-direction: column;
|
|
702
|
+
gap: 6px;
|
|
703
|
+
padding: 12px 6px 6px 2px;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.chat-message {
|
|
707
|
+
overflow-wrap: anywhere;
|
|
708
|
+
border-radius: 3px;
|
|
709
|
+
padding: 6px 8px;
|
|
710
|
+
max-width: 90%;
|
|
711
|
+
font-size: 1em;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/* User/error text is NOT markdown-rendered - preserve its whitespace. */
|
|
715
|
+
.chat-user,
|
|
716
|
+
.chat-error {
|
|
717
|
+
white-space: pre-wrap;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
.chat-user {
|
|
721
|
+
align-self: flex-end;
|
|
722
|
+
background-color: var(--psp--background-color);
|
|
723
|
+
border: 1px solid var(--psp-inactive--color, #6e6e6e);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
.chat-assistant {
|
|
727
|
+
align-self: flex-start;
|
|
728
|
+
background-color: #00000010;
|
|
729
|
+
border: 1px solid var(--psp-inactive--color, #6e6e6e);
|
|
730
|
+
|
|
731
|
+
/* Markdown content (see `agent/markdown.rs`). */
|
|
732
|
+
p,
|
|
733
|
+
ul,
|
|
734
|
+
ol,
|
|
735
|
+
blockquote,
|
|
736
|
+
pre,
|
|
737
|
+
table,
|
|
738
|
+
h1,
|
|
739
|
+
h2,
|
|
740
|
+
h3,
|
|
741
|
+
h4,
|
|
742
|
+
h5,
|
|
743
|
+
h6,
|
|
744
|
+
hr {
|
|
745
|
+
margin: 0.4em 0;
|
|
746
|
+
|
|
747
|
+
&:first-child {
|
|
748
|
+
margin-top: 0;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
&:last-child {
|
|
752
|
+
margin-bottom: 0;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
h1,
|
|
757
|
+
h2,
|
|
758
|
+
h3,
|
|
759
|
+
h4,
|
|
760
|
+
h5,
|
|
761
|
+
h6 {
|
|
762
|
+
font-size: 1.05em;
|
|
763
|
+
font-weight: 700;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
ul,
|
|
767
|
+
ol {
|
|
768
|
+
padding-inline-start: 1.5em;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
code {
|
|
772
|
+
font-family: inherit;
|
|
773
|
+
background-color: #00000028;
|
|
774
|
+
border-radius: 3px;
|
|
775
|
+
padding: 0 3px;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
pre {
|
|
779
|
+
overflow-x: auto;
|
|
780
|
+
background-color: #00000028;
|
|
781
|
+
border-radius: 3px;
|
|
782
|
+
padding: 6px 8px;
|
|
783
|
+
|
|
784
|
+
code {
|
|
785
|
+
background-color: transparent;
|
|
786
|
+
padding: 0;
|
|
787
|
+
white-space: pre;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
blockquote {
|
|
792
|
+
border-left: 2px solid var(--psp-inactive--color, #6e6e6e);
|
|
793
|
+
padding-left: 8px;
|
|
794
|
+
color: var(--psp-inactive--color, #6e6e6e);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
hr {
|
|
798
|
+
border: none;
|
|
799
|
+
border-top: 1px solid var(--psp-inactive--color, #6e6e6e);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
a {
|
|
803
|
+
color: var(--psp-active--color, #1a7da1);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
table {
|
|
807
|
+
border-collapse: collapse;
|
|
808
|
+
|
|
809
|
+
th,
|
|
810
|
+
td {
|
|
811
|
+
border: 1px solid var(--psp-inactive--color, #6e6e6e);
|
|
812
|
+
padding: 2px 6px;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
th {
|
|
816
|
+
font-weight: 700;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
.chat-md-align-center {
|
|
821
|
+
text-align: center;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
.chat-md-align-right {
|
|
825
|
+
text-align: right;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
.chat-error {
|
|
830
|
+
align-self: flex-start;
|
|
831
|
+
color: var(--warning--color, #ff471e);
|
|
832
|
+
border: 1px dashed var(--warning--color, #ff471e);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
.chat-tool-chip {
|
|
836
|
+
align-self: flex-start;
|
|
837
|
+
font-size: 0.66em;
|
|
838
|
+
text-transform: uppercase;
|
|
839
|
+
color: var(--psp-inactive--color, #6e6e6e);
|
|
840
|
+
border: 1px solid var(--psp-inactive--color, #6e6e6e);
|
|
841
|
+
border-radius: 3px;
|
|
842
|
+
padding: 1px 6px;
|
|
843
|
+
cursor: default;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
.chat-tool-chip-error {
|
|
847
|
+
color: var(--warning--color, #ff471e);
|
|
848
|
+
border-color: var(--warning--color, #ff471e);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
.chat-reasoning {
|
|
852
|
+
font-size: 0.9em;
|
|
853
|
+
color: var(--psp-inactive--color, #6e6e6e);
|
|
854
|
+
margin-bottom: 0.4em;
|
|
855
|
+
|
|
856
|
+
summary {
|
|
857
|
+
cursor: pointer;
|
|
858
|
+
user-select: none;
|
|
859
|
+
&:before {
|
|
860
|
+
text-transform: uppercase;
|
|
861
|
+
content: var(--psp-label--chat-reasoning--content);
|
|
862
|
+
font-size: var(--psp-label--font-size, 0.75em);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/* The BODY scrolls, not the `<details>` - the summary is the
|
|
867
|
+
* collapse toggle, and auto-scrolling an outer scroller to the
|
|
868
|
+
* bottom would carry it out of view. */
|
|
869
|
+
.chat-reasoning-body {
|
|
870
|
+
white-space: pre-wrap;
|
|
871
|
+
margin-top: 0.4em;
|
|
872
|
+
max-height: 96px;
|
|
873
|
+
overflow-y: auto;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
.chat-pending span {
|
|
878
|
+
display: inline-block;
|
|
879
|
+
width: 4px;
|
|
880
|
+
height: 4px;
|
|
881
|
+
margin-right: 3px;
|
|
882
|
+
border-radius: 50%;
|
|
883
|
+
background-color: var(--psp-inactive--color, #6e6e6e);
|
|
884
|
+
animation: chat-pending-pulse 1.2s infinite ease-in-out;
|
|
885
|
+
|
|
886
|
+
&:nth-child(2) {
|
|
887
|
+
animation-delay: 0.2s;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
&:nth-child(3) {
|
|
891
|
+
animation-delay: 0.4s;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
#chat_input_row {
|
|
896
|
+
display: flex;
|
|
897
|
+
gap: 6px;
|
|
898
|
+
flex: 0 0 auto;
|
|
899
|
+
padding-right: 12px;
|
|
900
|
+
|
|
901
|
+
/* The input grows downward as it fills; the button stays anchored
|
|
902
|
+
* to the bottom rather than stretching with it. */
|
|
903
|
+
align-items: flex-end;
|
|
904
|
+
|
|
905
|
+
/* The prompt is a `MirroredTextarea` (see
|
|
906
|
+
* `form/mirrored-textarea.css`): it is exactly as tall as its
|
|
907
|
+
* text, so this wrapper owns the visible box and caps how far
|
|
908
|
+
* that growth goes before it scrolls instead. */
|
|
909
|
+
#chat_input_scroll {
|
|
910
|
+
flex: 1 1 auto;
|
|
911
|
+
min-width: 0;
|
|
912
|
+
display: flex;
|
|
913
|
+
flex-direction: column;
|
|
914
|
+
min-height: 40px;
|
|
915
|
+
max-height: 160px;
|
|
916
|
+
overflow-y: auto;
|
|
917
|
+
border: 1px solid var(--psp-inactive--color, #6e6e6e);
|
|
918
|
+
border-radius: 3px;
|
|
919
|
+
|
|
920
|
+
&:focus-within {
|
|
921
|
+
border-color: var(--psp-active--color, #1a7da1);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/* Fills the resting box so clicks anywhere in it reach the
|
|
925
|
+
* input, then grows past it with the text. */
|
|
926
|
+
.chat-input-box {
|
|
927
|
+
flex: 1 0 auto;
|
|
928
|
+
padding: 4px 6px;
|
|
929
|
+
font-size: 1em;
|
|
930
|
+
|
|
931
|
+
/* The mirror paints the text; the input layer above it is
|
|
932
|
+
* caret-only (`mirrored-textarea.css`). */
|
|
933
|
+
.mirrored-textarea-mirror {
|
|
934
|
+
color: inherit;
|
|
935
|
+
|
|
936
|
+
/* The placeholder, as CSS `content` so the intl
|
|
937
|
+
* label mechanism can localize it - the native
|
|
938
|
+
* `placeholder` attribute is untranslatable and is
|
|
939
|
+
* kept only for assistive tech. */
|
|
940
|
+
&.is-empty:before {
|
|
941
|
+
content: var(--psp-label--chat-placeholder--content);
|
|
942
|
+
color: var(--psp-inactive--color, #6e6e6e);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/* The wrapper is the scroller - the input must not
|
|
947
|
+
* scroll independently of the text it mirrors. */
|
|
948
|
+
.mirrored-textarea-input {
|
|
949
|
+
overflow: hidden;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
button {
|
|
955
|
+
flex: 0 0 auto;
|
|
956
|
+
cursor: pointer;
|
|
957
|
+
font-family: inherit;
|
|
958
|
+
font-size: var(--label--font-size, 0.75em);
|
|
959
|
+
text-transform: uppercase;
|
|
960
|
+
color: inherit;
|
|
961
|
+
background-color: #00000020;
|
|
962
|
+
border: 1px solid var(--psp-inactive--color, #6e6e6e);
|
|
963
|
+
border-radius: 3px;
|
|
964
|
+
padding: 0 10px;
|
|
965
|
+
|
|
966
|
+
/* Matches `#chat_input`'s resting `min-height`: the row no
|
|
967
|
+
* longer stretches its children, so this keeps the button
|
|
968
|
+
* square with an empty input and anchored as it grows. */
|
|
969
|
+
height: 40px;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
#chat_send_button:before {
|
|
973
|
+
content: var(--psp-label--chat-send--content);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
#chat_stop_button:before {
|
|
977
|
+
content: var(--psp-label--chat-stop--content);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
#chat_badge {
|
|
982
|
+
flex: 0 0 auto;
|
|
983
|
+
font-size: 0.66em;
|
|
984
|
+
color: var(--psp-inactive--color, #6e6e6e);
|
|
985
|
+
overflow: hidden;
|
|
986
|
+
white-space: nowrap;
|
|
987
|
+
text-overflow: ellipsis;
|
|
988
|
+
margin-bottom: 6px;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
@keyframes chat-pending-pulse {
|
|
993
|
+
0%,
|
|
994
|
+
80%,
|
|
995
|
+
100% {
|
|
996
|
+
opacity: 0.25;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
40% {
|
|
1000
|
+
opacity: 1;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
pub mod protocol;
|
|
14
|
+
pub mod transport;
|
|
15
|
+
pub mod turn;
|
|
16
|
+
|
|
17
|
+
pub use protocol::ChatMessage;
|
|
18
|
+
pub use transport::{AgentTransport, OnDelta};
|
|
19
|
+
pub use turn::{TurnError, run_turn};
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
//! The OpenAI chat-completions wire shapes — only the fields this client
|
|
14
|
+
//! produces or consumes. Unknown response fields are ignored by serde, so
|
|
15
|
+
//! provider-specific extras (`usage`, `finish_reason`, ids) cost nothing.
|
|
16
|
+
//! The format is pinned by the `test/js/agent/` specs, which assert these
|
|
17
|
+
//! exact shapes against a scripted fake engine.
|
|
18
|
+
|
|
19
|
+
use serde::{Deserialize, Serialize};
|
|
20
|
+
use serde_json::Value;
|
|
21
|
+
|
|
22
|
+
/// One conversation message, tagged by `role`. `Assistant` doubles as the
|
|
23
|
+
/// response-message shape (`choices[n].message`).
|
|
24
|
+
#[derive(Clone, Deserialize, Serialize)]
|
|
25
|
+
#[serde(tag = "role", rename_all = "lowercase")]
|
|
26
|
+
pub enum ChatMessage {
|
|
27
|
+
System {
|
|
28
|
+
content: String,
|
|
29
|
+
},
|
|
30
|
+
User {
|
|
31
|
+
content: String,
|
|
32
|
+
},
|
|
33
|
+
Assistant {
|
|
34
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
35
|
+
content: Option<String>,
|
|
36
|
+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
37
|
+
tool_calls: Vec<ToolCall>,
|
|
38
|
+
|
|
39
|
+
/// Reasoning-model "thinking" text, captured for DISPLAY only:
|
|
40
|
+
/// `skip_serializing` is the contract that it is never replayed
|
|
41
|
+
/// into a request (providers reject or mis-tokenize replayed
|
|
42
|
+
/// reasoning). Two wire conventions, one field: DeepSeek-style
|
|
43
|
+
/// `reasoning_content` (also LM Studio/vLLM) and OpenRouter's
|
|
44
|
+
/// `reasoning`.
|
|
45
|
+
#[serde(default, skip_serializing, alias = "reasoning")]
|
|
46
|
+
reasoning_content: Option<String>,
|
|
47
|
+
},
|
|
48
|
+
Tool {
|
|
49
|
+
tool_call_id: String,
|
|
50
|
+
content: String,
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// A model-emitted tool invocation, echoed back verbatim in the transcript
|
|
55
|
+
/// so the model can correlate `Tool` results by `id`.
|
|
56
|
+
#[derive(Clone, Deserialize, Serialize)]
|
|
57
|
+
pub struct ToolCall {
|
|
58
|
+
#[serde(default)]
|
|
59
|
+
pub id: String,
|
|
60
|
+
#[serde(default = "function_type", rename = "type")]
|
|
61
|
+
pub kind: String,
|
|
62
|
+
pub function: FunctionCall,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// `arguments` is a JSON-*encoded string* per the OpenAI wire format, not a
|
|
66
|
+
/// JSON object.
|
|
67
|
+
#[derive(Clone, Deserialize, Serialize)]
|
|
68
|
+
pub struct FunctionCall {
|
|
69
|
+
pub name: String,
|
|
70
|
+
#[serde(default)]
|
|
71
|
+
pub arguments: String,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fn function_type() -> String {
|
|
75
|
+
"function".to_owned()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// A tool advertised to the model.
|
|
79
|
+
#[derive(Serialize)]
|
|
80
|
+
pub struct ToolDef {
|
|
81
|
+
#[serde(rename = "type")]
|
|
82
|
+
pub kind: &'static str,
|
|
83
|
+
pub function: FunctionDef,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[derive(Serialize)]
|
|
87
|
+
pub struct FunctionDef {
|
|
88
|
+
pub name: &'static str,
|
|
89
|
+
pub description: &'static str,
|
|
90
|
+
pub parameters: Value,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#[derive(Serialize)]
|
|
94
|
+
pub struct ChatRequest<'a> {
|
|
95
|
+
pub model: &'a str,
|
|
96
|
+
pub messages: &'a [ChatMessage],
|
|
97
|
+
pub tools: &'a [ToolDef],
|
|
98
|
+
|
|
99
|
+
/// Always `true`; servers that ignore it get the buffered fallback in
|
|
100
|
+
/// `transport.rs`, so streaming needs no configuration surface.
|
|
101
|
+
pub stream: bool,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#[derive(Deserialize)]
|
|
105
|
+
pub struct ChatResponse {
|
|
106
|
+
pub choices: Vec<ChatChoice>,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#[derive(Deserialize)]
|
|
110
|
+
pub struct ChatChoice {
|
|
111
|
+
pub message: ChatMessage,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// One `stream: true` SSE frame (`data: {...}`) — the delta shapes of the
|
|
115
|
+
/// OpenAI streaming wire format.
|
|
116
|
+
#[derive(Deserialize)]
|
|
117
|
+
pub struct ChatChunk {
|
|
118
|
+
#[serde(default)]
|
|
119
|
+
pub choices: Vec<ChunkChoice>,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#[derive(Deserialize)]
|
|
123
|
+
pub struct ChunkChoice {
|
|
124
|
+
#[serde(default)]
|
|
125
|
+
pub delta: ChunkDelta,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#[derive(Default, Deserialize)]
|
|
129
|
+
pub struct ChunkDelta {
|
|
130
|
+
#[serde(default)]
|
|
131
|
+
pub content: Option<String>,
|
|
132
|
+
#[serde(default)]
|
|
133
|
+
pub reasoning_content: Option<String>,
|
|
134
|
+
#[serde(default)]
|
|
135
|
+
pub reasoning: Option<String>,
|
|
136
|
+
#[serde(default)]
|
|
137
|
+
pub tool_calls: Vec<ToolCallDelta>,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Tool-call fragments accumulate BY INDEX: `id`/`name` arrive on the
|
|
141
|
+
/// first fragment, `arguments` arrives as string parts across many.
|
|
142
|
+
#[derive(Deserialize)]
|
|
143
|
+
pub struct ToolCallDelta {
|
|
144
|
+
#[serde(default)]
|
|
145
|
+
pub index: usize,
|
|
146
|
+
#[serde(default)]
|
|
147
|
+
pub id: Option<String>,
|
|
148
|
+
#[serde(default)]
|
|
149
|
+
pub function: Option<FunctionCallDelta>,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[derive(Deserialize)]
|
|
153
|
+
pub struct FunctionCallDelta {
|
|
154
|
+
#[serde(default)]
|
|
155
|
+
pub name: Option<String>,
|
|
156
|
+
#[serde(default)]
|
|
157
|
+
pub arguments: Option<String>,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// Folds a stream of [`ChatChunk`]s into the same
|
|
161
|
+
/// [`ChatMessage::Assistant`] the buffered path produces, so `turn.rs` is
|
|
162
|
+
/// agnostic to how the response arrived. Reasoning takes the first
|
|
163
|
+
/// non-empty of the two wire conventions per delta.
|
|
164
|
+
#[derive(Default)]
|
|
165
|
+
pub struct ChunkAccumulator {
|
|
166
|
+
content: String,
|
|
167
|
+
reasoning: String,
|
|
168
|
+
tool_calls: Vec<ToolCall>,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
impl ChunkAccumulator {
|
|
172
|
+
pub fn push(&mut self, chunk: ChatChunk) {
|
|
173
|
+
for choice in chunk.choices {
|
|
174
|
+
let delta = choice.delta;
|
|
175
|
+
if let Some(content) = delta.content {
|
|
176
|
+
self.content.push_str(&content);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if let Some(reasoning) = delta
|
|
180
|
+
.reasoning_content
|
|
181
|
+
.into_iter()
|
|
182
|
+
.chain(delta.reasoning)
|
|
183
|
+
.find(|x| !x.is_empty())
|
|
184
|
+
{
|
|
185
|
+
self.reasoning.push_str(&reasoning);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for fragment in delta.tool_calls {
|
|
189
|
+
if self.tool_calls.len() <= fragment.index {
|
|
190
|
+
self.tool_calls
|
|
191
|
+
.resize_with(fragment.index + 1, || ToolCall {
|
|
192
|
+
id: String::new(),
|
|
193
|
+
kind: function_type(),
|
|
194
|
+
function: FunctionCall {
|
|
195
|
+
name: String::new(),
|
|
196
|
+
arguments: String::new(),
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let call = &mut self.tool_calls[fragment.index];
|
|
202
|
+
if let Some(id) = fragment.id {
|
|
203
|
+
call.id = id;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if let Some(function) = fragment.function {
|
|
207
|
+
if let Some(name) = function.name {
|
|
208
|
+
call.function.name = name;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if let Some(arguments) = function.arguments {
|
|
212
|
+
call.function.arguments.push_str(&arguments);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// Accumulated text/reasoning so far, for live-render callbacks.
|
|
220
|
+
pub fn progress(&self) -> (&str, &str) {
|
|
221
|
+
(&self.content, &self.reasoning)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
pub fn finish(self) -> ChatMessage {
|
|
225
|
+
ChatMessage::Assistant {
|
|
226
|
+
content: (!self.content.is_empty()).then_some(self.content),
|
|
227
|
+
tool_calls: self.tool_calls,
|
|
228
|
+
reasoning_content: (!self.reasoning.is_empty()).then_some(self.reasoning),
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#[cfg(test)]
|
|
234
|
+
mod tests {
|
|
235
|
+
use super::*;
|
|
236
|
+
|
|
237
|
+
fn chunk(json: &str) -> ChatChunk {
|
|
238
|
+
serde_json::from_str(json).unwrap()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
#[test]
|
|
242
|
+
fn content_and_both_reasoning_conventions_fold() {
|
|
243
|
+
let mut acc = ChunkAccumulator::default();
|
|
244
|
+
acc.push(chunk(r#"{"choices":[{"delta":{"role":"assistant"}}]}"#));
|
|
245
|
+
acc.push(chunk(
|
|
246
|
+
r#"{"choices":[{"delta":{"reasoning_content":"th"}}]}"#,
|
|
247
|
+
));
|
|
248
|
+
acc.push(chunk(r#"{"choices":[{"delta":{"reasoning":"ink"}}]}"#));
|
|
249
|
+
acc.push(chunk(r#"{"choices":[{"delta":{"content":"Hel"}}]}"#));
|
|
250
|
+
acc.push(chunk(
|
|
251
|
+
r#"{"choices":[{"delta":{"content":"lo","reasoning":""}}]}"#,
|
|
252
|
+
));
|
|
253
|
+
assert_eq!(acc.progress(), ("Hello", "think"));
|
|
254
|
+
let ChatMessage::Assistant {
|
|
255
|
+
content,
|
|
256
|
+
tool_calls,
|
|
257
|
+
reasoning_content,
|
|
258
|
+
} = acc.finish()
|
|
259
|
+
else {
|
|
260
|
+
panic!("not assistant");
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
assert_eq!(content.as_deref(), Some("Hello"));
|
|
264
|
+
assert!(tool_calls.is_empty());
|
|
265
|
+
assert_eq!(reasoning_content.as_deref(), Some("think"));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
#[test]
|
|
269
|
+
fn fragmented_tool_arguments_accumulate_by_index() {
|
|
270
|
+
let mut acc = ChunkAccumulator::default();
|
|
271
|
+
acc.push(chunk(
|
|
272
|
+
r#"{"choices":[{"delta":{"tool_calls":[
|
|
273
|
+
{"index":0,"id":"c0","function":{"name":"get_schema","arguments":""}}]}}]}"#,
|
|
274
|
+
));
|
|
275
|
+
|
|
276
|
+
// Index 1 opens before index 0 finishes; both argument streams
|
|
277
|
+
// interleave.
|
|
278
|
+
acc.push(chunk(
|
|
279
|
+
r#"{"choices":[{"delta":{"tool_calls":[
|
|
280
|
+
{"index":1,"id":"c1","function":{"name":"set_view_config","arguments":"{\"con"}}]}}]}"#,
|
|
281
|
+
));
|
|
282
|
+
|
|
283
|
+
acc.push(chunk(
|
|
284
|
+
r#"{"choices":[{"delta":{"tool_calls":[
|
|
285
|
+
{"index":1,"function":{"arguments":"fig\":{}}"}}]}}]}"#,
|
|
286
|
+
));
|
|
287
|
+
|
|
288
|
+
let ChatMessage::Assistant { tool_calls, .. } = acc.finish() else {
|
|
289
|
+
panic!("not assistant");
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
assert_eq!(tool_calls.len(), 2);
|
|
293
|
+
assert_eq!(tool_calls[0].id, "c0");
|
|
294
|
+
assert_eq!(tool_calls[0].function.name, "get_schema");
|
|
295
|
+
assert_eq!(tool_calls[1].function.arguments, "{\"config\":{}}");
|
|
296
|
+
assert_eq!(tool_calls[1].kind, "function");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/// The display-only contract: reasoning deserializes from EITHER wire
|
|
300
|
+
/// field but NEVER serializes back into a request.
|
|
301
|
+
#[test]
|
|
302
|
+
fn assistant_reasoning_reads_both_aliases_and_never_serializes() {
|
|
303
|
+
for field in ["reasoning_content", "reasoning"] {
|
|
304
|
+
let message: ChatMessage = serde_json::from_str(&format!(
|
|
305
|
+
r#"{{"role":"assistant","content":"hi","{field}":"secret"}}"#
|
|
306
|
+
))
|
|
307
|
+
.unwrap();
|
|
308
|
+
|
|
309
|
+
let ChatMessage::Assistant {
|
|
310
|
+
reasoning_content, ..
|
|
311
|
+
} = &message
|
|
312
|
+
else {
|
|
313
|
+
panic!("not assistant");
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
assert_eq!(reasoning_content.as_deref(), Some("secret"));
|
|
317
|
+
assert!(!serde_json::to_string(&message).unwrap().contains("secret"));
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|