@adminforth/agent 1.39.0 → 1.40.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.
@@ -1,9 +1,14 @@
1
1
  import { logger } from "adminforth";
2
2
  import type { PluginOptions } from "../types.js";
3
3
 
4
- export type UserLanguage = {
4
+ export type DetectedLanguage = {
5
5
  language: string;
6
- code: string;
6
+ code: string; // ISO 639-1
7
+ ambiguous: boolean;
8
+ };
9
+
10
+ export type PreviousUserMessage = {
11
+ text: string;
7
12
  };
8
13
 
9
14
  const USER_LANGUAGE_OUTPUT_SCHEMA = {
@@ -19,23 +24,28 @@ const USER_LANGUAGE_OUTPUT_SCHEMA = {
19
24
  },
20
25
  code: {
21
26
  type: "string",
22
- description: "Uppercase two-letter language code, for example EN, UA, FR.",
27
+ description: "Uppercase ISO 639-1 two-letter language code, for example EN, UK, FR.",
28
+ },
29
+ ambiguous: {
30
+ type: "boolean",
31
+ description: "True if the user's language cannot be confidently detected from the message.",
23
32
  },
24
33
  },
25
- required: ["language", "code"],
34
+ required: ["language", "code", "ambiguous"],
26
35
  },
27
36
  } as const;
28
37
 
29
- function parseUserLanguage(content: string | undefined): UserLanguage | null {
38
+ function parseUserLanguage(content: string | undefined): DetectedLanguage | null {
30
39
  if (!content) {
31
40
  return null;
32
41
  }
33
42
 
34
43
  try {
35
- const parsed = JSON.parse(content) as UserLanguage;
44
+ const parsed = JSON.parse(content) as DetectedLanguage;
36
45
  return {
37
46
  language: parsed.language,
38
47
  code: parsed.code,
48
+ ambiguous: parsed.ambiguous,
39
49
  };
40
50
  } catch (error) {
41
51
  logger.warn(`Failed to parse detected user language: ${error instanceof Error ? error.message : String(error)}`);
@@ -46,15 +56,26 @@ function parseUserLanguage(content: string | undefined): UserLanguage | null {
46
56
  export async function detectUserLanguage(
47
57
  completionAdapter: PluginOptions["modes"][number]["completionAdapter"],
48
58
  prompt: string,
49
- ): Promise<UserLanguage | null> {
59
+ previousUserMessages: PreviousUserMessage[] = [],
60
+ ): Promise<DetectedLanguage | null> {
61
+ const previousMessages = previousUserMessages.length
62
+ ? [
63
+ "",
64
+ "Previous user messages:",
65
+ ...previousUserMessages.map((message) => message.text),
66
+ ]
67
+ : [];
50
68
  const response = await completionAdapter.complete({
51
69
  content: [
52
- "Detect the language of the user's message.",
70
+ "Detect the language the assistant should use for the current user message.",
71
+ "Use recent conversation context only to resolve short or ambiguous current messages.",
53
72
  "Return only the requested structured output.",
54
73
  "The language must be the full English language name.",
55
- "The code must be an uppercase two-letter code like EN, UA, FR.",
74
+ "The code must be an uppercase ISO 639-1 two-letter code like UK, EN, FR.",
75
+ "Set ambiguous to true if the response language cannot be confidently detected from the current message or context.",
76
+ ...previousMessages,
56
77
  "",
57
- "User message:",
78
+ "Current user message:",
58
79
  prompt,
59
80
  ].join("\n"),
60
81
  maxTokens: 80,
@@ -1,5 +1,5 @@
1
1
  import type { AdminForthResource, AdminUser, IAdminForth } from "adminforth";
2
- import type { UserLanguage } from "./languageDetect.js";
2
+ import type { DetectedLanguage } from "./languageDetect.js";
3
3
  import {
4
4
  listBundledSkillManifests,
5
5
  listProjectSkillManifests,
@@ -46,8 +46,8 @@ export function appendCustomSystemPrompt(
46
46
  return `${systemPrompt}\n\n${normalizedCustomSystemPrompt}`;
47
47
  }
48
48
 
49
- function formatLanguagePrompt(language: UserLanguage | null) {
50
- if (!language) {
49
+ function formatLanguagePrompt(language: DetectedLanguage | null) {
50
+ if (!language || language.ambiguous) {
51
51
  return "Respond in the user's language.";
52
52
  }
53
53
 
@@ -70,7 +70,7 @@ export function buildAgentTurnSystemPrompt(input: {
70
70
  agentSystemPrompt: string;
71
71
  adminUser: AdminUser;
72
72
  usernameField: string;
73
- userLanguage: UserLanguage | null;
73
+ userLanguage: DetectedLanguage | null;
74
74
  }) {
75
75
  return [
76
76
  input.agentSystemPrompt,
package/build.log CHANGED
@@ -58,5 +58,5 @@ custom/speech_recognition_frontend/voiceActivityDetection.ts
58
58
  custom/speech_recognition_frontend/types/
59
59
  custom/speech_recognition_frontend/types/voice-activity-detection.d.ts
60
60
 
61
- sent 1,656,225 bytes received 860 bytes 3,314,170.00 bytes/sec
62
- total size is 1,652,309 speedup is 1.00
61
+ sent 1,655,890 bytes received 860 bytes 3,313,500.00 bytes/sec
62
+ total size is 1,651,974 speedup is 1.00
@@ -119,8 +119,8 @@
119
119
  }"
120
120
  >
121
121
  <div
122
- class="w-full border rounded-lg pb-8 dark:bg-gray-700"
123
- :class="agentStore.isAudioChatMode ? 'border-none mt-8' : 'border'"
122
+ class="w-full border rounded-lg pb-8"
123
+ :class="agentStore.isAudioChatMode ? 'border-none mt-8' : 'border dark:bg-gray-700'"
124
124
  >
125
125
  <textarea
126
126
  v-if="!agentStore.isAudioChatMode"
@@ -10,7 +10,7 @@ Then you need to select between two main tools for fetching data:
10
10
 
11
11
  - Load and call `aggregate` tool to fetch data for analytics. This is the main tool for fast server-side aggregations, including filtered data, grouped metrics, and date buckets such as day, week, or month. Always prioritize this way of fetching data for analytics, as it is optimized for performance and reduces the amount of data transferred and processed in-memory.
12
12
 
13
- - Load and call `get_resource_data` tool only when the requested analysis cannot be answered with `aggregate`. This is heavier because it returns original rows with all fields, but it allows complex calculations, comparisons, and custom groupings in-memory.
13
+ - Load and call `get_resource_data` tool only when the requested analysis cannot be answered with `aggregate`. When using it, pass `columns` with only the fields required for the calculation so large result sets do not include unrelated row data.
14
14
 
15
15
  # Instructions
16
16
 
@@ -19,6 +19,7 @@ When the user asks for analytics, reports, trends, comparisons, or distributions
19
19
  - Fetch the requested data using `aggregate` whenever possible.
20
20
  - If it is not possible to get the required aggregates using `aggregate`, fetch the underlying rows with `get_resource_data`.
21
21
  - Prefer narrow requests: use filters, sorting, pagination, and date ranges whenever possible.
22
+ - Prefer narrow row payloads: include `columns` when only one or a few fields are needed.
22
23
  - If the request is ambiguous, clarify the resource, metric, grouping, or date range before fetching data.
23
24
  - Return a short written summary with the key finding and most important numbers.
24
25
  - If the user asks for a chart, or if a chart would help and you decide to produce one, invoke the `charts` skill for chart formatting and Vega-Lite requirements.
@@ -6,6 +6,8 @@ description: Fetch one or more records. Use to find records entities. To use thi
6
6
 
7
7
  You can use tool `get_resource_data` it returns one or more records and is capable of using filters
8
8
 
9
+ When you only need specific fields, pass `columns` with the exact resource column names you need. This keeps large result sets small. For record links or user-facing record choices, include the primary key and enough display fields, or omit `columns` when you need the full row context.
10
+
9
11
  # Instructions
10
12
 
11
13
  To find specific data record you should use filters. ILIKE filters are preferred when we are unsure the input is clear.You can combine filters with OR if you want to search multiple fields.If user queries one record you should try to fetch up to 5 records and if more then one returned return output them all to user and ask to select one. When you communicate about record with user, show its several most important fields.
@@ -1,97 +1,43 @@
1
- <template>
2
- <div
3
- class=" bg-white w-[0.2rem] rounded-sm transition-all duration-300 ease-in-out"
4
- :class="{
5
- 'recordingAnimation1' : showAnimation,
6
- 'h-2': !isRecording,
7
- 'h-1': isRecording,
8
- }"
1
+ <template>
2
+ <div
3
+ v-for="(height, index) in lineHeights"
4
+ :key="index"
5
+ class="bg-white w-[0.2rem] rounded-sm transition-all duration-100 ease-out"
6
+ :style="{ height }"
9
7
  />
10
- <div
11
- class=" bg-white w-[0.2rem] rounded-sm transition-all duration-300 ease-in-out"
12
- :class="{
13
- 'recordingAnimation2' : showAnimation,
14
- 'h-4': !isRecording,
15
- 'h-1': isRecording,
16
- }"
17
- />
18
- <div
19
- class=" bg-white w-[0.2rem] rounded-sm transition-all duration-300 ease-in-out"
20
- :class="{
21
- 'recordingAnimation3' : showAnimation,
22
- 'h-3': !isRecording,
23
- 'h-1': isRecording,
24
- }"
25
- />
26
- <div
27
- class=" bg-white w-[0.2rem] rounded-sm transition-all duration-300 ease-in-out"
28
- :class="{
29
- 'recordingAnimation4' : showAnimation,
30
- 'h-2': !isRecording,
31
- 'h-1': isRecording,
32
- }"
33
- />
34
- <template v-if="isRecording">
35
- <div
36
- class=" bg-white w-[0.2rem] rounded-sm h-1 transition-all duration-300 ease-in-out"
37
- :class="{
38
- 'recordingAnimation5' : showAnimation,
39
- }"
40
- />
41
- <p class="text-white ml-2">End</p>
42
- </template>
8
+ <p v-if="isRecording" class="text-white ml-2">End</p>
43
9
  </template>
44
10
 
45
-
46
-
47
11
  <script setup lang="ts">
12
+ import { computed } from 'vue';
13
+
14
+ const IDLE_LINE_HEIGHTS = [0.5, 1, 0.75, 0.5];
15
+ const RECORDING_LINE_WEIGHTS = [0.45, 1, 0.75, 0.9, 0.55];
16
+ const MIN_RECORDING_HEIGHT = 0.25;
17
+ const MAX_RECORDING_DELTA = 0.9;
48
18
 
49
19
  const props = defineProps<{
50
20
  showAnimation: boolean;
51
21
  isRecording: boolean;
22
+ amplitude: number;
52
23
  }>();
53
24
 
54
- </script>
55
-
56
- <style scoped lang="scss">
57
- .recordingAnimation1 {
58
- animation: recordingAnimation 1s infinite;
59
- height: 0.3rem;
60
- }
61
-
62
- .recordingAnimation2 {
63
- animation: recordingAnimation 1s infinite;
64
- animation-delay: 0.2s;
65
- height: 0.5rem;
25
+ const normalizedAmplitude = computed(() => {
26
+ if (!props.isRecording || !props.showAnimation) {
27
+ return 0;
66
28
  }
67
29
 
68
- .recordingAnimation3 {
69
- animation: recordingAnimation 1s infinite;
70
- animation-delay: 0.4s;
71
- height: 0.4rem;
72
- }
73
-
74
- .recordingAnimation4 {
75
- animation: recordingAnimation 1s infinite;
76
- animation-delay: 0.6s;
77
- height: 0.5rem;
78
- }
30
+ return Math.min(Math.max(props.amplitude, 0), 1);
31
+ });
79
32
 
80
- .recordingAnimation5 {
81
- animation: recordingAnimation 1s infinite;
82
- animation-delay: 0.8s;
83
- height: 0.3rem;
33
+ const lineHeights = computed(() => {
34
+ if (!props.isRecording) {
35
+ return IDLE_LINE_HEIGHTS.map((height) => `${height}rem`);
84
36
  }
85
37
 
86
- @keyframes recordingAnimation {
87
- 0% {
88
- transform: scaleY(1);
89
- }
90
- 50% {
91
- transform: scaleY(2);
92
- }
93
- 100% {
94
- transform: scaleY(1);
95
- }
96
- }
97
- </style>
38
+ return RECORDING_LINE_WEIGHTS.map((weight) => {
39
+ const height = MIN_RECORDING_HEIGHT + normalizedAmplitude.value * MAX_RECORDING_DELTA * weight;
40
+ return `${height}rem`;
41
+ });
42
+ });
43
+ </script>
@@ -8,7 +8,11 @@
8
8
  >
9
9
  <div class="w-5 h-5 flex items-center justify-center">
10
10
  <div v-if="microphoneButtonMode === 'listen' || microphoneButtonMode === 'off'" class="flex justify-evenly items-center gap-[0.1rem]">
11
- <AudioLines :showAnimation="showAudioWavesAnimation" :isRecording="microphoneButtonMode === 'listen'" />
11
+ <AudioLines
12
+ :showAnimation="showAudioWavesAnimation"
13
+ :isRecording="microphoneButtonMode === 'listen'"
14
+ :amplitude="audioAmplitude"
15
+ />
12
16
  </div>
13
17
  <div v-else-if="microphoneButtonMode === 'generating'" class="flex items-center justify-center gap-2 text-white text-sm">
14
18
  <span class="w-3 h-3 bg-white rounded-sm" />
@@ -39,6 +43,7 @@ const { stopCurrentAudioPlayback } = agentAudio;
39
43
  const { agentAudioMode } = storeToRefs(agentAudio);
40
44
  const microphoneButtonMode = ref<'off' | 'calibrating' | 'listen' | 'transcribing' | 'generating'>('off');
41
45
  const showAudioWavesAnimation = ref(false);
46
+ const audioAmplitude = ref(0);
42
47
  const hideAnimationDebounced = debounce(() => {
43
48
  showAudioWavesAnimation.value = false;
44
49
  }, 100);
@@ -91,20 +96,24 @@ async function onStartRecording() {
91
96
  microphoneButtonMode.value = 'calibrating';
92
97
  await requestMicAndStartVAD(saidSomething, stopRecording, onAnySound);
93
98
  setTimeout(() => {
94
- microphoneButtonMode.value = 'listen';
95
- agentAudio.playBeep(1000);
99
+ if (isAudioChatMode.value) {
100
+ microphoneButtonMode.value = 'listen';
101
+ agentAudio.playBeep(1000);
102
+ }
96
103
  }, CALIBRATION_DURATION);
97
104
  }
98
105
 
99
106
  function onStopRecording() {
100
107
  agentAudio.playBeep(600);
101
108
  stopUserMedia();
109
+ audioAmplitude.value = 0;
102
110
  showAudioWavesAnimation.value = false;
103
111
  }
104
112
 
105
113
  function resetAll() {
106
114
  stopGenerationAndAudio();
107
115
  microphoneButtonMode.value = 'off';
116
+ audioAmplitude.value = 0;
108
117
  showAudioWavesAnimation.value = false;
109
118
  hideAnimationDebounced.cancel();
110
119
  sendUserRecordDebounced.cancel();
@@ -123,7 +132,10 @@ function stopRecording() {
123
132
  }
124
133
 
125
134
  function onAnySound(amplitude: number) {
135
+ audioAmplitude.value = Math.min(Math.max(amplitude, 0), 1);
136
+
126
137
  if(amplitude < 0.01) {
138
+ audioAmplitude.value = 0;
127
139
  showAudioWavesAnimation.value = false;
128
140
  return;
129
141
  }
@@ -21,10 +21,14 @@ const USER_LANGUAGE_OUTPUT_SCHEMA = {
21
21
  },
22
22
  code: {
23
23
  type: "string",
24
- description: "Uppercase two-letter language code, for example EN, UA, FR.",
24
+ description: "Uppercase ISO 639-1 two-letter language code, for example EN, UK, FR.",
25
+ },
26
+ ambiguous: {
27
+ type: "boolean",
28
+ description: "True if the user's language cannot be confidently detected from the message.",
25
29
  },
26
30
  },
27
- required: ["language", "code"],
31
+ required: ["language", "code", "ambiguous"],
28
32
  },
29
33
  };
30
34
  function parseUserLanguage(content) {
@@ -36,6 +40,7 @@ function parseUserLanguage(content) {
36
40
  return {
37
41
  language: parsed.language,
38
42
  code: parsed.code,
43
+ ambiguous: parsed.ambiguous,
39
44
  };
40
45
  }
41
46
  catch (error) {
@@ -43,16 +48,26 @@ function parseUserLanguage(content) {
43
48
  return null;
44
49
  }
45
50
  }
46
- export function detectUserLanguage(completionAdapter, prompt) {
47
- return __awaiter(this, void 0, void 0, function* () {
51
+ export function detectUserLanguage(completionAdapter_1, prompt_1) {
52
+ return __awaiter(this, arguments, void 0, function* (completionAdapter, prompt, previousUserMessages = []) {
53
+ const previousMessages = previousUserMessages.length
54
+ ? [
55
+ "",
56
+ "Previous user messages:",
57
+ ...previousUserMessages.map((message) => message.text),
58
+ ]
59
+ : [];
48
60
  const response = yield completionAdapter.complete({
49
61
  content: [
50
- "Detect the language of the user's message.",
62
+ "Detect the language the assistant should use for the current user message.",
63
+ "Use recent conversation context only to resolve short or ambiguous current messages.",
51
64
  "Return only the requested structured output.",
52
65
  "The language must be the full English language name.",
53
- "The code must be an uppercase two-letter code like EN, UA, FR.",
66
+ "The code must be an uppercase ISO 639-1 two-letter code like UK, EN, FR.",
67
+ "Set ambiguous to true if the response language cannot be confidently detected from the current message or context.",
68
+ ...previousMessages,
54
69
  "",
55
- "User message:",
70
+ "Current user message:",
56
71
  prompt,
57
72
  ].join("\n"),
58
73
  maxTokens: 80,
@@ -38,7 +38,7 @@ export function appendCustomSystemPrompt(systemPrompt, customSystemPrompt) {
38
38
  return `${systemPrompt}\n\n${normalizedCustomSystemPrompt}`;
39
39
  }
40
40
  function formatLanguagePrompt(language) {
41
- if (!language) {
41
+ if (!language || language.ambiguous) {
42
42
  return "Respond in the user's language.";
43
43
  }
44
44
  return `Respond in ${language.language} (${language.code}).`;
@@ -119,8 +119,8 @@
119
119
  }"
120
120
  >
121
121
  <div
122
- class="w-full border rounded-lg pb-8 dark:bg-gray-700"
123
- :class="agentStore.isAudioChatMode ? 'border-none mt-8' : 'border'"
122
+ class="w-full border rounded-lg pb-8"
123
+ :class="agentStore.isAudioChatMode ? 'border-none mt-8' : 'border dark:bg-gray-700'"
124
124
  >
125
125
  <textarea
126
126
  v-if="!agentStore.isAudioChatMode"
@@ -10,7 +10,7 @@ Then you need to select between two main tools for fetching data:
10
10
 
11
11
  - Load and call `aggregate` tool to fetch data for analytics. This is the main tool for fast server-side aggregations, including filtered data, grouped metrics, and date buckets such as day, week, or month. Always prioritize this way of fetching data for analytics, as it is optimized for performance and reduces the amount of data transferred and processed in-memory.
12
12
 
13
- - Load and call `get_resource_data` tool only when the requested analysis cannot be answered with `aggregate`. This is heavier because it returns original rows with all fields, but it allows complex calculations, comparisons, and custom groupings in-memory.
13
+ - Load and call `get_resource_data` tool only when the requested analysis cannot be answered with `aggregate`. When using it, pass `columns` with only the fields required for the calculation so large result sets do not include unrelated row data.
14
14
 
15
15
  # Instructions
16
16
 
@@ -19,6 +19,7 @@ When the user asks for analytics, reports, trends, comparisons, or distributions
19
19
  - Fetch the requested data using `aggregate` whenever possible.
20
20
  - If it is not possible to get the required aggregates using `aggregate`, fetch the underlying rows with `get_resource_data`.
21
21
  - Prefer narrow requests: use filters, sorting, pagination, and date ranges whenever possible.
22
+ - Prefer narrow row payloads: include `columns` when only one or a few fields are needed.
22
23
  - If the request is ambiguous, clarify the resource, metric, grouping, or date range before fetching data.
23
24
  - Return a short written summary with the key finding and most important numbers.
24
25
  - If the user asks for a chart, or if a chart would help and you decide to produce one, invoke the `charts` skill for chart formatting and Vega-Lite requirements.
@@ -6,6 +6,8 @@ description: Fetch one or more records. Use to find records entities. To use thi
6
6
 
7
7
  You can use tool `get_resource_data` it returns one or more records and is capable of using filters
8
8
 
9
+ When you only need specific fields, pass `columns` with the exact resource column names you need. This keeps large result sets small. For record links or user-facing record choices, include the primary key and enough display fields, or omit `columns` when you need the full row context.
10
+
9
11
  # Instructions
10
12
 
11
13
  To find specific data record you should use filters. ILIKE filters are preferred when we are unsure the input is clear.You can combine filters with OR if you want to search multiple fields.If user queries one record you should try to fetch up to 5 records and if more then one returned return output them all to user and ask to select one. When you communicate about record with user, show its several most important fields.
@@ -1,97 +1,43 @@
1
- <template>
2
- <div
3
- class=" bg-white w-[0.2rem] rounded-sm transition-all duration-300 ease-in-out"
4
- :class="{
5
- 'recordingAnimation1' : showAnimation,
6
- 'h-2': !isRecording,
7
- 'h-1': isRecording,
8
- }"
1
+ <template>
2
+ <div
3
+ v-for="(height, index) in lineHeights"
4
+ :key="index"
5
+ class="bg-white w-[0.2rem] rounded-sm transition-all duration-100 ease-out"
6
+ :style="{ height }"
9
7
  />
10
- <div
11
- class=" bg-white w-[0.2rem] rounded-sm transition-all duration-300 ease-in-out"
12
- :class="{
13
- 'recordingAnimation2' : showAnimation,
14
- 'h-4': !isRecording,
15
- 'h-1': isRecording,
16
- }"
17
- />
18
- <div
19
- class=" bg-white w-[0.2rem] rounded-sm transition-all duration-300 ease-in-out"
20
- :class="{
21
- 'recordingAnimation3' : showAnimation,
22
- 'h-3': !isRecording,
23
- 'h-1': isRecording,
24
- }"
25
- />
26
- <div
27
- class=" bg-white w-[0.2rem] rounded-sm transition-all duration-300 ease-in-out"
28
- :class="{
29
- 'recordingAnimation4' : showAnimation,
30
- 'h-2': !isRecording,
31
- 'h-1': isRecording,
32
- }"
33
- />
34
- <template v-if="isRecording">
35
- <div
36
- class=" bg-white w-[0.2rem] rounded-sm h-1 transition-all duration-300 ease-in-out"
37
- :class="{
38
- 'recordingAnimation5' : showAnimation,
39
- }"
40
- />
41
- <p class="text-white ml-2">End</p>
42
- </template>
8
+ <p v-if="isRecording" class="text-white ml-2">End</p>
43
9
  </template>
44
10
 
45
-
46
-
47
11
  <script setup lang="ts">
12
+ import { computed } from 'vue';
13
+
14
+ const IDLE_LINE_HEIGHTS = [0.5, 1, 0.75, 0.5];
15
+ const RECORDING_LINE_WEIGHTS = [0.45, 1, 0.75, 0.9, 0.55];
16
+ const MIN_RECORDING_HEIGHT = 0.25;
17
+ const MAX_RECORDING_DELTA = 0.9;
48
18
 
49
19
  const props = defineProps<{
50
20
  showAnimation: boolean;
51
21
  isRecording: boolean;
22
+ amplitude: number;
52
23
  }>();
53
24
 
54
- </script>
55
-
56
- <style scoped lang="scss">
57
- .recordingAnimation1 {
58
- animation: recordingAnimation 1s infinite;
59
- height: 0.3rem;
60
- }
61
-
62
- .recordingAnimation2 {
63
- animation: recordingAnimation 1s infinite;
64
- animation-delay: 0.2s;
65
- height: 0.5rem;
25
+ const normalizedAmplitude = computed(() => {
26
+ if (!props.isRecording || !props.showAnimation) {
27
+ return 0;
66
28
  }
67
29
 
68
- .recordingAnimation3 {
69
- animation: recordingAnimation 1s infinite;
70
- animation-delay: 0.4s;
71
- height: 0.4rem;
72
- }
73
-
74
- .recordingAnimation4 {
75
- animation: recordingAnimation 1s infinite;
76
- animation-delay: 0.6s;
77
- height: 0.5rem;
78
- }
30
+ return Math.min(Math.max(props.amplitude, 0), 1);
31
+ });
79
32
 
80
- .recordingAnimation5 {
81
- animation: recordingAnimation 1s infinite;
82
- animation-delay: 0.8s;
83
- height: 0.3rem;
33
+ const lineHeights = computed(() => {
34
+ if (!props.isRecording) {
35
+ return IDLE_LINE_HEIGHTS.map((height) => `${height}rem`);
84
36
  }
85
37
 
86
- @keyframes recordingAnimation {
87
- 0% {
88
- transform: scaleY(1);
89
- }
90
- 50% {
91
- transform: scaleY(2);
92
- }
93
- 100% {
94
- transform: scaleY(1);
95
- }
96
- }
97
- </style>
38
+ return RECORDING_LINE_WEIGHTS.map((weight) => {
39
+ const height = MIN_RECORDING_HEIGHT + normalizedAmplitude.value * MAX_RECORDING_DELTA * weight;
40
+ return `${height}rem`;
41
+ });
42
+ });
43
+ </script>
@@ -8,7 +8,11 @@
8
8
  >
9
9
  <div class="w-5 h-5 flex items-center justify-center">
10
10
  <div v-if="microphoneButtonMode === 'listen' || microphoneButtonMode === 'off'" class="flex justify-evenly items-center gap-[0.1rem]">
11
- <AudioLines :showAnimation="showAudioWavesAnimation" :isRecording="microphoneButtonMode === 'listen'" />
11
+ <AudioLines
12
+ :showAnimation="showAudioWavesAnimation"
13
+ :isRecording="microphoneButtonMode === 'listen'"
14
+ :amplitude="audioAmplitude"
15
+ />
12
16
  </div>
13
17
  <div v-else-if="microphoneButtonMode === 'generating'" class="flex items-center justify-center gap-2 text-white text-sm">
14
18
  <span class="w-3 h-3 bg-white rounded-sm" />
@@ -39,6 +43,7 @@ const { stopCurrentAudioPlayback } = agentAudio;
39
43
  const { agentAudioMode } = storeToRefs(agentAudio);
40
44
  const microphoneButtonMode = ref<'off' | 'calibrating' | 'listen' | 'transcribing' | 'generating'>('off');
41
45
  const showAudioWavesAnimation = ref(false);
46
+ const audioAmplitude = ref(0);
42
47
  const hideAnimationDebounced = debounce(() => {
43
48
  showAudioWavesAnimation.value = false;
44
49
  }, 100);
@@ -91,20 +96,24 @@ async function onStartRecording() {
91
96
  microphoneButtonMode.value = 'calibrating';
92
97
  await requestMicAndStartVAD(saidSomething, stopRecording, onAnySound);
93
98
  setTimeout(() => {
94
- microphoneButtonMode.value = 'listen';
95
- agentAudio.playBeep(1000);
99
+ if (isAudioChatMode.value) {
100
+ microphoneButtonMode.value = 'listen';
101
+ agentAudio.playBeep(1000);
102
+ }
96
103
  }, CALIBRATION_DURATION);
97
104
  }
98
105
 
99
106
  function onStopRecording() {
100
107
  agentAudio.playBeep(600);
101
108
  stopUserMedia();
109
+ audioAmplitude.value = 0;
102
110
  showAudioWavesAnimation.value = false;
103
111
  }
104
112
 
105
113
  function resetAll() {
106
114
  stopGenerationAndAudio();
107
115
  microphoneButtonMode.value = 'off';
116
+ audioAmplitude.value = 0;
108
117
  showAudioWavesAnimation.value = false;
109
118
  hideAnimationDebounced.cancel();
110
119
  sendUserRecordDebounced.cancel();
@@ -123,7 +132,10 @@ function stopRecording() {
123
132
  }
124
133
 
125
134
  function onAnySound(amplitude: number) {
135
+ audioAmplitude.value = Math.min(Math.max(amplitude, 0), 1);
136
+
126
137
  if(amplitude < 0.01) {
138
+ audioAmplitude.value = 0;
127
139
  showAudioWavesAnimation.value = false;
128
140
  return;
129
141
  }
package/dist/index.js CHANGED
@@ -78,6 +78,16 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
78
78
  }));
79
79
  });
80
80
  }
81
+ getPreviousUserMessages(sessionId) {
82
+ return __awaiter(this, void 0, void 0, function* () {
83
+ const turns = yield this.adminforth.resource(this.options.turnResource.resourceId).list([Filters.EQ(this.options.turnResource.sessionIdField, sessionId)], 2, undefined, [Sorts.DESC(this.options.turnResource.createdAtField)]);
84
+ return turns
85
+ .reverse()
86
+ .map((turn) => ({
87
+ text: turn[this.options.turnResource.promptField],
88
+ }));
89
+ });
90
+ }
81
91
  getCheckpointer() {
82
92
  if (this.checkpointer)
83
93
  return this.checkpointer;
@@ -160,7 +170,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
160
170
  const model = primaryModelSpec.model;
161
171
  const summaryModel = summaryModelSpec.model;
162
172
  const modelMiddleware = primaryModelSpec.middleware;
163
- const userLanguage = yield detectUserLanguage(selectedMode.completionAdapter, input.prompt)
173
+ const userLanguage = yield detectUserLanguage(selectedMode.completionAdapter, input.prompt, input.previousUserMessages)
164
174
  .catch((error) => {
165
175
  logger.warn(`Failed to detect user language: ${error.message}`);
166
176
  return null;
@@ -247,6 +257,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
247
257
  runAndPersistAgentResponse(input) {
248
258
  return __awaiter(this, void 0, void 0, function* () {
249
259
  var _a, _b;
260
+ const previousUserMessages = yield this.getPreviousUserMessages(input.sessionId);
250
261
  const turnId = yield this.createNewTurn(input.sessionId, input.prompt);
251
262
  yield this.adminforth.resource(this.options.sessionResource.resourceId).update(input.sessionId, {
252
263
  [this.options.sessionResource.createdAtField]: new Date().toISOString(),
@@ -260,6 +271,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
260
271
  prompt: input.prompt,
261
272
  sessionId: input.sessionId,
262
273
  turnId,
274
+ previousUserMessages,
263
275
  modeName: input.modeName,
264
276
  userTimeZone: input.userTimeZone,
265
277
  currentPage: input.currentPage,
@@ -376,14 +388,24 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
376
388
  filename: req.file.originalname,
377
389
  mimeType: req.file.mimetype,
378
390
  language: "auto",
391
+ abortSignal,
379
392
  });
380
393
  }
381
394
  catch (error) {
395
+ if (abortSignal.aborted) {
396
+ logger.info("Agent speech transcription aborted by the client");
397
+ stream.end();
398
+ return null;
399
+ }
382
400
  logger.error(`Agent speech transcription failed:\n${error.message}`);
383
401
  stream.error("Speech transcription failed. Check server logs for details.");
384
402
  stream.end();
385
403
  return null;
386
404
  }
405
+ if (abortSignal.aborted) {
406
+ stream.end();
407
+ return null;
408
+ }
387
409
  const prompt = transcription.text;
388
410
  if (!prompt) {
389
411
  stream.error("Speech transcription is empty");
@@ -426,19 +448,32 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
426
448
  stream: true,
427
449
  streamFormat: "audio",
428
450
  format: "mp3",
451
+ abortSignal,
429
452
  });
430
453
  stream.audioStart(speech.mimeType, speech.format);
431
454
  const reader = speech.audioStream.getReader();
455
+ const cancelAudioStream = () => {
456
+ void reader.cancel();
457
+ };
432
458
  try {
459
+ abortSignal.addEventListener("abort", cancelAudioStream, { once: true });
433
460
  while (true) {
461
+ if (abortSignal.aborted) {
462
+ yield reader.cancel();
463
+ break;
464
+ }
434
465
  const { value, done } = yield reader.read();
435
466
  if (done) {
436
467
  break;
437
468
  }
469
+ if (abortSignal.aborted) {
470
+ break;
471
+ }
438
472
  stream.audioDelta(value);
439
473
  }
440
474
  }
441
475
  finally {
476
+ abortSignal.removeEventListener("abort", cancelAudioStream);
442
477
  reader.releaseLock();
443
478
  }
444
479
  stream.audioDone();
package/index.ts CHANGED
@@ -10,7 +10,7 @@ import { z } from "zod";
10
10
  import { createAgentChatModel, callAgent } from "./agent/simpleAgent.js";
11
11
  import { AdminForthCheckpointSaver } from "./agent/checkpointer.js";
12
12
  import { createSequenceDebugCollector } from "./agent/middleware/sequenceDebug.js";
13
- import { detectUserLanguage } from "./agent/languageDetect.js";
13
+ import { detectUserLanguage, type PreviousUserMessage } from "./agent/languageDetect.js";
14
14
  import { prepareApiBasedTools as buildApiBasedTools } from './apiBasedTools.js';
15
15
  import { createAgentEventStream } from "./agentResponseEvents.js";
16
16
  import { appendCustomSystemPrompt, buildAgentSystemPrompt, buildAgentTurnSystemPrompt, DEFAULT_AGENT_SYSTEM_PROMPT} from "./agent/systemPrompt.js";
@@ -29,6 +29,7 @@ type AgentTurnRunInput = {
29
29
  prompt: string;
30
30
  sessionId: string;
31
31
  turnId: string;
32
+ previousUserMessages: PreviousUserMessage[];
32
33
  modeName?: string | null;
33
34
  userTimeZone: string;
34
35
  currentPage?: CurrentPageContext;
@@ -41,7 +42,7 @@ type AgentTurnRunInput = {
41
42
  };
42
43
 
43
44
  type RunAndPersistAgentResponseInput =
44
- Omit<AgentTurnRunInput, "turnId" | "sequenceDebugCollector"> & {
45
+ Omit<AgentTurnRunInput, "turnId" | "sequenceDebugCollector" | "previousUserMessages"> & {
45
46
  emitErrorResponse?: (response: string) => void;
46
47
  failureLogMessage: string;
47
48
  abortLogMessage: string;
@@ -116,6 +117,20 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
116
117
  }));
117
118
  }
118
119
 
120
+ private async getPreviousUserMessages(sessionId: string) {
121
+ const turns = await this.adminforth.resource(this.options.turnResource.resourceId).list(
122
+ [Filters.EQ(this.options.turnResource.sessionIdField, sessionId)],
123
+ 2,
124
+ undefined,
125
+ [Sorts.DESC(this.options.turnResource.createdAtField)]
126
+ );
127
+ return turns
128
+ .reverse()
129
+ .map((turn): PreviousUserMessage => ({
130
+ text: turn[this.options.turnResource.promptField],
131
+ }));
132
+ }
133
+
119
134
  private getCheckpointer() {
120
135
  if (this.checkpointer) return this.checkpointer;
121
136
 
@@ -199,7 +214,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
199
214
  const summaryModel = summaryModelSpec.model;
200
215
  const modelMiddleware = primaryModelSpec.middleware;
201
216
 
202
- const userLanguage = await detectUserLanguage(selectedMode.completionAdapter, input.prompt)
217
+ const userLanguage = await detectUserLanguage(selectedMode.completionAdapter, input.prompt, input.previousUserMessages)
203
218
  .catch((error) => {
204
219
  logger.warn(`Failed to detect user language: ${error.message}`);
205
220
  return null;
@@ -284,6 +299,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
284
299
  }
285
300
 
286
301
  private async runAndPersistAgentResponse(input: RunAndPersistAgentResponseInput) {
302
+ const previousUserMessages = await this.getPreviousUserMessages(input.sessionId);
287
303
  const turnId = await this.createNewTurn(input.sessionId, input.prompt);
288
304
  await this.adminforth.resource(this.options.sessionResource.resourceId).update(input.sessionId, {
289
305
  [this.options.sessionResource.createdAtField]: new Date().toISOString(),
@@ -298,6 +314,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
298
314
  prompt: input.prompt,
299
315
  sessionId: input.sessionId,
300
316
  turnId,
317
+ previousUserMessages,
301
318
  modeName: input.modeName,
302
319
  userTimeZone: input.userTimeZone,
303
320
  currentPage: input.currentPage,
@@ -417,14 +434,26 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
417
434
  filename: req.file.originalname,
418
435
  mimeType: req.file.mimetype,
419
436
  language: "auto",
437
+ abortSignal,
420
438
  });
421
439
  } catch (error) {
440
+ if (abortSignal.aborted) {
441
+ logger.info("Agent speech transcription aborted by the client");
442
+ stream.end();
443
+ return null;
444
+ }
445
+
422
446
  logger.error(`Agent speech transcription failed:\n${error.message}`);
423
447
  stream.error("Speech transcription failed. Check server logs for details.");
424
448
  stream.end();
425
449
  return null;
426
450
  }
427
451
 
452
+ if (abortSignal.aborted) {
453
+ stream.end();
454
+ return null;
455
+ }
456
+
428
457
  const prompt = transcription.text;
429
458
  if (!prompt) {
430
459
  stream.error("Speech transcription is empty");
@@ -476,23 +505,39 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
476
505
  stream: true,
477
506
  streamFormat: "audio",
478
507
  format: "mp3",
508
+ abortSignal,
479
509
  });
480
510
 
481
511
  stream.audioStart(speech.mimeType, speech.format);
482
512
 
483
513
  const reader = speech.audioStream.getReader();
514
+ const cancelAudioStream = () => {
515
+ void reader.cancel();
516
+ };
484
517
 
485
518
  try {
519
+ abortSignal.addEventListener("abort", cancelAudioStream, { once: true });
520
+
486
521
  while (true) {
522
+ if (abortSignal.aborted) {
523
+ await reader.cancel();
524
+ break;
525
+ }
526
+
487
527
  const { value, done } = await reader.read();
488
528
 
489
529
  if (done) {
490
530
  break;
491
531
  }
492
532
 
533
+ if (abortSignal.aborted) {
534
+ break;
535
+ }
536
+
493
537
  stream.audioDelta(value);
494
538
  }
495
539
  } finally {
540
+ abortSignal.removeEventListener("abort", cancelAudioStream);
496
541
  reader.releaseLock();
497
542
  }
498
543
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/agent",
3
- "version": "1.39.0",
3
+ "version": "1.40.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -33,7 +33,7 @@
33
33
  "@langchain/core": "^1.1.40",
34
34
  "@langchain/langgraph": "^1.2.8",
35
35
  "@langchain/langgraph-checkpoint": "^1.0.1",
36
- "adminforth": "^2.52.0",
36
+ "adminforth": "^2.53.5",
37
37
  "dayjs": "^1.11.20",
38
38
  "langchain": "^1.3.3",
39
39
  "multer": "^2.1.1",