@juspay/neurolink 9.10.0 → 9.10.1

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/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [9.10.1](https://github.com/juspay/neurolink/compare/v9.10.0...v9.10.1) (2026-02-21)
2
+
3
+ ### Bug Fixes
4
+
5
+ - **(video-analysis):** add stream support for video analysis ([938aeef](https://github.com/juspay/neurolink/commit/938aeef876277360700d2a7192155af1f1316f28))
6
+
1
7
  ## [9.10.0](https://github.com/juspay/neurolink/compare/v9.9.0...v9.10.0) (2026-02-20)
2
8
 
3
9
  ### Features
@@ -8,16 +8,16 @@
8
8
  */
9
9
  import { AIProviderName } from "../../constants/enums.js";
10
10
  import type { CoreMessage } from "ai";
11
- export declare function analyzeVideoWithVertexAI(frames: CoreMessage, options?: {
11
+ export declare function analyzeVideoWithVertexAI(messages: CoreMessage[], options?: {
12
12
  project?: string;
13
13
  location?: string;
14
14
  model?: string;
15
15
  }): Promise<string>;
16
- export declare function analyzeVideoWithGeminiAPI(frames: CoreMessage, options?: {
16
+ export declare function analyzeVideoWithGeminiAPI(messages: CoreMessage[], options?: {
17
17
  apiKey?: string;
18
18
  model?: string;
19
19
  }): Promise<string>;
20
- export declare function analyzeVideo(frames: CoreMessage, options?: {
20
+ export declare function analyzeVideo(messages: CoreMessage[], options?: {
21
21
  provider?: AIProviderName;
22
22
  project?: string;
23
23
  location?: string;
@@ -9,23 +9,35 @@
9
9
  import { AIProviderName, ErrorSeverity, ErrorCategory, } from "../../constants/enums.js";
10
10
  import { logger } from "../../utils/logger.js";
11
11
  import { readFile } from "node:fs/promises";
12
- import { NeuroLinkError } from "../../utils/errorHandling.js";
12
+ import { NeuroLinkError, ErrorFactory } from "../../utils/errorHandling.js";
13
13
  // ---------------------------------------------------------------------------
14
14
  // Shared config
15
15
  // ---------------------------------------------------------------------------
16
16
  const DEFAULT_MODEL = "gemini-2.0-flash";
17
17
  const DEFAULT_LOCATION = "us-central1";
18
+ /**
19
+ * Extract content items from user messages
20
+ *
21
+ * @param messages - Array of CoreMessage objects
22
+ * @returns Flattened array of content items from user messages
23
+ */
24
+ function extractUserContent(messages) {
25
+ const userMessages = messages.filter((msg) => msg.role === "user");
26
+ return userMessages.flatMap((msg) => Array.isArray(msg.content) ? msg.content : []);
27
+ }
18
28
  /**
19
29
  * Convert CoreMessage content array to Gemini parts format
20
30
  *
21
- * @param contentArray - Array of content items from CoreMessage
31
+ * @param messages - Array of CoreMessage objects
22
32
  * @returns Array of parts in Gemini API format
23
33
  */
24
- function buildContentParts(frames) {
25
- const contentArray = Array.isArray(frames.content) ? frames.content : [];
26
- return contentArray.map((item) => {
27
- if (item.type === "text" && item.text) {
28
- return { text: item.text };
34
+ function buildContentParts(messages) {
35
+ const allContent = extractUserContent(messages);
36
+ return allContent
37
+ .map((item) => {
38
+ if (item.type === "text") {
39
+ // Accept text parts regardless of whether text is empty
40
+ return { text: item.text || "" };
29
41
  }
30
42
  else if (item.type === "image" && item.image) {
31
43
  let base64Data;
@@ -38,7 +50,7 @@ function buildContentParts(frames) {
38
50
  base64Data = item.image.replace(/^data:image\/[a-z]+;base64,/, "");
39
51
  }
40
52
  else {
41
- throw new Error(`Invalid image data type: expected string, Buffer, or Uint8Array, got ${typeof item.image}`);
53
+ throw ErrorFactory.invalidConfiguration("image data type", `expected string, Buffer, or Uint8Array, got ${typeof item.image}`, { itemType: item.type, dataType: typeof item.image });
42
54
  }
43
55
  return {
44
56
  inlineData: {
@@ -47,8 +59,14 @@ function buildContentParts(frames) {
47
59
  },
48
60
  };
49
61
  }
50
- throw new Error(`Invalid content type: ${item.type}`);
51
- });
62
+ else if (item.type === "file") {
63
+ // Skip file parts - not supported in Gemini parts format
64
+ return null;
65
+ }
66
+ // Return null for unsupported types
67
+ return null;
68
+ })
69
+ .filter((part) => part !== null);
52
70
  }
53
71
  /**
54
72
  * Configuration for video frame analysis.
@@ -88,7 +106,7 @@ Ensure the final response is fully self-sufficient and does not reference extern
88
106
  // ---------------------------------------------------------------------------
89
107
  // Vertex AI
90
108
  // ---------------------------------------------------------------------------
91
- export async function analyzeVideoWithVertexAI(frames, options = {}) {
109
+ export async function analyzeVideoWithVertexAI(messages, options = {}) {
92
110
  const startTime = Date.now();
93
111
  const { GoogleGenAI } = await import("@google/genai");
94
112
  // Get default config and merge with provided options
@@ -96,9 +114,9 @@ export async function analyzeVideoWithVertexAI(frames, options = {}) {
96
114
  const project = options.project ?? config.project;
97
115
  const location = options.location ?? config.location;
98
116
  const model = options.model || DEFAULT_MODEL;
99
- // Extract content array from CoreMessage
100
- const contentArray = Array.isArray(frames.content) ? frames.content : [];
101
- const frameCount = contentArray.filter((item) => item.type === "image").length;
117
+ // Convert frames content to parts array for Gemini
118
+ const parts = buildContentParts(messages);
119
+ const frameCount = parts.filter((part) => "inlineData" in part && part.inlineData).length;
102
120
  logger.debug("[GeminiVideoAnalyzer] Analyzing video with Vertex AI", {
103
121
  project,
104
122
  location,
@@ -106,8 +124,6 @@ export async function analyzeVideoWithVertexAI(frames, options = {}) {
106
124
  frameCount,
107
125
  });
108
126
  const ai = new GoogleGenAI({ vertexai: true, project, location });
109
- // Convert frames content to parts array for Gemini
110
- const parts = buildContentParts(frames);
111
127
  const response = await ai.models.generateContent({
112
128
  model,
113
129
  config: buildConfig(),
@@ -129,7 +145,7 @@ export async function analyzeVideoWithVertexAI(frames, options = {}) {
129
145
  // ---------------------------------------------------------------------------
130
146
  // Gemini API (Google AI)
131
147
  // ---------------------------------------------------------------------------
132
- export async function analyzeVideoWithGeminiAPI(frames, options = {}) {
148
+ export async function analyzeVideoWithGeminiAPI(messages, options = {}) {
133
149
  const startTime = Date.now();
134
150
  const { GoogleGenAI } = await import("@google/genai");
135
151
  const apiKey = options.apiKey || process.env.GOOGLE_AI_API_KEY;
@@ -137,16 +153,14 @@ export async function analyzeVideoWithGeminiAPI(frames, options = {}) {
137
153
  if (!apiKey) {
138
154
  throw new Error("GOOGLE_AI_API_KEY environment variable is required for Gemini API video analysis");
139
155
  }
140
- // Extract content array from CoreMessage
141
- const contentArray = Array.isArray(frames.content) ? frames.content : [];
142
- const frameCount = contentArray.filter((item) => item.type === "image").length;
156
+ // Convert frames content to parts array for Gemini
157
+ const parts = buildContentParts(messages);
158
+ const frameCount = parts.filter((part) => "inlineData" in part && part.inlineData).length;
143
159
  logger.debug("[GeminiVideoAnalyzer] Analyzing video with Gemini API", {
144
160
  model,
145
161
  frameCount,
146
162
  });
147
163
  const ai = new GoogleGenAI({ apiKey });
148
- // Convert frames content to parts array for Gemini
149
- const parts = buildContentParts(frames);
150
164
  logger.debug("[GeminiVideoAnalyzer] Generating analysis with frames");
151
165
  const response = await ai.models.generateContent({
152
166
  model,
@@ -207,15 +221,15 @@ async function getVertexConfig() {
207
221
  }
208
222
  return { project, location };
209
223
  }
210
- export async function analyzeVideo(frames, options = {}) {
224
+ export async function analyzeVideo(messages, options = {}) {
211
225
  const provider = options.provider || AIProviderName.AUTO;
212
226
  // Vertex — only when GOOGLE_VERTEX_PROJECT is explicitly set
213
227
  if (provider === AIProviderName.VERTEX || provider === AIProviderName.AUTO) {
214
- return analyzeVideoWithVertexAI(frames, options);
228
+ return analyzeVideoWithVertexAI(messages, options);
215
229
  }
216
230
  // Gemini API — when GOOGLE_AI_API_KEY is set
217
231
  if (provider === AIProviderName.GOOGLE_AI && process.env.GOOGLE_AI_API_KEY) {
218
- return analyzeVideoWithGeminiAPI(frames, options);
232
+ return analyzeVideoWithGeminiAPI(messages, options);
219
233
  }
220
234
  throw new Error("No valid provider configuration found. " +
221
235
  "Set GOOGLE_VERTEX_PROJECT for Vertex AI or GOOGLE_AI_API_KEY for Gemini API.");
@@ -86,6 +86,20 @@ export class BaseProvider {
86
86
  temperature: options.temperature,
87
87
  timestamp: Date.now(),
88
88
  });
89
+ // ===== EARLY MULTIMODAL DETECTION =====
90
+ const hasFileInput = !!options.input?.files?.length || !!options.input?.videoFiles?.length;
91
+ if (hasFileInput) {
92
+ // ===== VIDEO ANALYSIS DETECTION =====
93
+ // Check if video frames are present and handle with fake streaming
94
+ const messages = await this.buildMessagesForStream(options);
95
+ if (hasVideoFrames(messages)) {
96
+ logger.info(`Video frames detected in stream, using fake streaming for video analysis`, {
97
+ provider: this.providerName,
98
+ model: this.modelName,
99
+ });
100
+ return await this.executeFakeStreaming(options, analysisSchema);
101
+ }
102
+ }
89
103
  // 🔧 CRITICAL: Image generation models don't support real streaming
90
104
  // Force fake streaming for image models to ensure image output is yielded
91
105
  const isImageModel = IMAGE_GENERATION_MODELS.some((m) => this.modelName.includes(m));
@@ -8,16 +8,16 @@
8
8
  */
9
9
  import { AIProviderName } from "../../constants/enums.js";
10
10
  import type { CoreMessage } from "ai";
11
- export declare function analyzeVideoWithVertexAI(frames: CoreMessage, options?: {
11
+ export declare function analyzeVideoWithVertexAI(messages: CoreMessage[], options?: {
12
12
  project?: string;
13
13
  location?: string;
14
14
  model?: string;
15
15
  }): Promise<string>;
16
- export declare function analyzeVideoWithGeminiAPI(frames: CoreMessage, options?: {
16
+ export declare function analyzeVideoWithGeminiAPI(messages: CoreMessage[], options?: {
17
17
  apiKey?: string;
18
18
  model?: string;
19
19
  }): Promise<string>;
20
- export declare function analyzeVideo(frames: CoreMessage, options?: {
20
+ export declare function analyzeVideo(messages: CoreMessage[], options?: {
21
21
  provider?: AIProviderName;
22
22
  project?: string;
23
23
  location?: string;
@@ -9,23 +9,35 @@
9
9
  import { AIProviderName, ErrorSeverity, ErrorCategory, } from "../../constants/enums.js";
10
10
  import { logger } from "../../utils/logger.js";
11
11
  import { readFile } from "node:fs/promises";
12
- import { NeuroLinkError } from "../../utils/errorHandling.js";
12
+ import { NeuroLinkError, ErrorFactory } from "../../utils/errorHandling.js";
13
13
  // ---------------------------------------------------------------------------
14
14
  // Shared config
15
15
  // ---------------------------------------------------------------------------
16
16
  const DEFAULT_MODEL = "gemini-2.0-flash";
17
17
  const DEFAULT_LOCATION = "us-central1";
18
+ /**
19
+ * Extract content items from user messages
20
+ *
21
+ * @param messages - Array of CoreMessage objects
22
+ * @returns Flattened array of content items from user messages
23
+ */
24
+ function extractUserContent(messages) {
25
+ const userMessages = messages.filter((msg) => msg.role === "user");
26
+ return userMessages.flatMap((msg) => Array.isArray(msg.content) ? msg.content : []);
27
+ }
18
28
  /**
19
29
  * Convert CoreMessage content array to Gemini parts format
20
30
  *
21
- * @param contentArray - Array of content items from CoreMessage
31
+ * @param messages - Array of CoreMessage objects
22
32
  * @returns Array of parts in Gemini API format
23
33
  */
24
- function buildContentParts(frames) {
25
- const contentArray = Array.isArray(frames.content) ? frames.content : [];
26
- return contentArray.map((item) => {
27
- if (item.type === "text" && item.text) {
28
- return { text: item.text };
34
+ function buildContentParts(messages) {
35
+ const allContent = extractUserContent(messages);
36
+ return allContent
37
+ .map((item) => {
38
+ if (item.type === "text") {
39
+ // Accept text parts regardless of whether text is empty
40
+ return { text: item.text || "" };
29
41
  }
30
42
  else if (item.type === "image" && item.image) {
31
43
  let base64Data;
@@ -38,7 +50,7 @@ function buildContentParts(frames) {
38
50
  base64Data = item.image.replace(/^data:image\/[a-z]+;base64,/, "");
39
51
  }
40
52
  else {
41
- throw new Error(`Invalid image data type: expected string, Buffer, or Uint8Array, got ${typeof item.image}`);
53
+ throw ErrorFactory.invalidConfiguration("image data type", `expected string, Buffer, or Uint8Array, got ${typeof item.image}`, { itemType: item.type, dataType: typeof item.image });
42
54
  }
43
55
  return {
44
56
  inlineData: {
@@ -47,8 +59,14 @@ function buildContentParts(frames) {
47
59
  },
48
60
  };
49
61
  }
50
- throw new Error(`Invalid content type: ${item.type}`);
51
- });
62
+ else if (item.type === "file") {
63
+ // Skip file parts - not supported in Gemini parts format
64
+ return null;
65
+ }
66
+ // Return null for unsupported types
67
+ return null;
68
+ })
69
+ .filter((part) => part !== null);
52
70
  }
53
71
  /**
54
72
  * Configuration for video frame analysis.
@@ -88,7 +106,7 @@ Ensure the final response is fully self-sufficient and does not reference extern
88
106
  // ---------------------------------------------------------------------------
89
107
  // Vertex AI
90
108
  // ---------------------------------------------------------------------------
91
- export async function analyzeVideoWithVertexAI(frames, options = {}) {
109
+ export async function analyzeVideoWithVertexAI(messages, options = {}) {
92
110
  const startTime = Date.now();
93
111
  const { GoogleGenAI } = await import("@google/genai");
94
112
  // Get default config and merge with provided options
@@ -96,9 +114,9 @@ export async function analyzeVideoWithVertexAI(frames, options = {}) {
96
114
  const project = options.project ?? config.project;
97
115
  const location = options.location ?? config.location;
98
116
  const model = options.model || DEFAULT_MODEL;
99
- // Extract content array from CoreMessage
100
- const contentArray = Array.isArray(frames.content) ? frames.content : [];
101
- const frameCount = contentArray.filter((item) => item.type === "image").length;
117
+ // Convert frames content to parts array for Gemini
118
+ const parts = buildContentParts(messages);
119
+ const frameCount = parts.filter((part) => "inlineData" in part && part.inlineData).length;
102
120
  logger.debug("[GeminiVideoAnalyzer] Analyzing video with Vertex AI", {
103
121
  project,
104
122
  location,
@@ -106,8 +124,6 @@ export async function analyzeVideoWithVertexAI(frames, options = {}) {
106
124
  frameCount,
107
125
  });
108
126
  const ai = new GoogleGenAI({ vertexai: true, project, location });
109
- // Convert frames content to parts array for Gemini
110
- const parts = buildContentParts(frames);
111
127
  const response = await ai.models.generateContent({
112
128
  model,
113
129
  config: buildConfig(),
@@ -129,7 +145,7 @@ export async function analyzeVideoWithVertexAI(frames, options = {}) {
129
145
  // ---------------------------------------------------------------------------
130
146
  // Gemini API (Google AI)
131
147
  // ---------------------------------------------------------------------------
132
- export async function analyzeVideoWithGeminiAPI(frames, options = {}) {
148
+ export async function analyzeVideoWithGeminiAPI(messages, options = {}) {
133
149
  const startTime = Date.now();
134
150
  const { GoogleGenAI } = await import("@google/genai");
135
151
  const apiKey = options.apiKey || process.env.GOOGLE_AI_API_KEY;
@@ -137,16 +153,14 @@ export async function analyzeVideoWithGeminiAPI(frames, options = {}) {
137
153
  if (!apiKey) {
138
154
  throw new Error("GOOGLE_AI_API_KEY environment variable is required for Gemini API video analysis");
139
155
  }
140
- // Extract content array from CoreMessage
141
- const contentArray = Array.isArray(frames.content) ? frames.content : [];
142
- const frameCount = contentArray.filter((item) => item.type === "image").length;
156
+ // Convert frames content to parts array for Gemini
157
+ const parts = buildContentParts(messages);
158
+ const frameCount = parts.filter((part) => "inlineData" in part && part.inlineData).length;
143
159
  logger.debug("[GeminiVideoAnalyzer] Analyzing video with Gemini API", {
144
160
  model,
145
161
  frameCount,
146
162
  });
147
163
  const ai = new GoogleGenAI({ apiKey });
148
- // Convert frames content to parts array for Gemini
149
- const parts = buildContentParts(frames);
150
164
  logger.debug("[GeminiVideoAnalyzer] Generating analysis with frames");
151
165
  const response = await ai.models.generateContent({
152
166
  model,
@@ -207,15 +221,15 @@ async function getVertexConfig() {
207
221
  }
208
222
  return { project, location };
209
223
  }
210
- export async function analyzeVideo(frames, options = {}) {
224
+ export async function analyzeVideo(messages, options = {}) {
211
225
  const provider = options.provider || AIProviderName.AUTO;
212
226
  // Vertex — only when GOOGLE_VERTEX_PROJECT is explicitly set
213
227
  if (provider === AIProviderName.VERTEX || provider === AIProviderName.AUTO) {
214
- return analyzeVideoWithVertexAI(frames, options);
228
+ return analyzeVideoWithVertexAI(messages, options);
215
229
  }
216
230
  // Gemini API — when GOOGLE_AI_API_KEY is set
217
231
  if (provider === AIProviderName.GOOGLE_AI && process.env.GOOGLE_AI_API_KEY) {
218
- return analyzeVideoWithGeminiAPI(frames, options);
232
+ return analyzeVideoWithGeminiAPI(messages, options);
219
233
  }
220
234
  throw new Error("No valid provider configuration found. " +
221
235
  "Set GOOGLE_VERTEX_PROJECT for Vertex AI or GOOGLE_AI_API_KEY for Gemini API.");
@@ -86,6 +86,20 @@ export class BaseProvider {
86
86
  temperature: options.temperature,
87
87
  timestamp: Date.now(),
88
88
  });
89
+ // ===== EARLY MULTIMODAL DETECTION =====
90
+ const hasFileInput = !!options.input?.files?.length || !!options.input?.videoFiles?.length;
91
+ if (hasFileInput) {
92
+ // ===== VIDEO ANALYSIS DETECTION =====
93
+ // Check if video frames are present and handle with fake streaming
94
+ const messages = await this.buildMessagesForStream(options);
95
+ if (hasVideoFrames(messages)) {
96
+ logger.info(`Video frames detected in stream, using fake streaming for video analysis`, {
97
+ provider: this.providerName,
98
+ model: this.modelName,
99
+ });
100
+ return await this.executeFakeStreaming(options, analysisSchema);
101
+ }
102
+ }
89
103
  // 🔧 CRITICAL: Image generation models don't support real streaming
90
104
  // Force fake streaming for image models to ensure image output is yielded
91
105
  const isImageModel = IMAGE_GENERATION_MODELS.some((m) => this.modelName.includes(m));
@@ -9,9 +9,10 @@ import type { CoreMessage } from "ai";
9
9
  import { AIProviderName } from "../constants/enums.js";
10
10
  /**
11
11
  * Check if messages contain video frames (images)
12
+ * Only checks user messages to match buildContentParts behavior
12
13
  *
13
14
  * @param messages - Array of CoreMessage objects
14
- * @returns true if video frames are present
15
+ * @returns true if video frames are present in user messages
15
16
  */
16
17
  export declare function hasVideoFrames(messages: CoreMessage[]): boolean;
17
18
  /**
@@ -9,12 +9,17 @@ import { AIProviderName } from "../constants/enums.js";
9
9
  import { logger } from "./logger.js";
10
10
  /**
11
11
  * Check if messages contain video frames (images)
12
+ * Only checks user messages to match buildContentParts behavior
12
13
  *
13
14
  * @param messages - Array of CoreMessage objects
14
- * @returns true if video frames are present
15
+ * @returns true if video frames are present in user messages
15
16
  */
16
17
  export function hasVideoFrames(messages) {
17
18
  return messages.some((msg) => {
19
+ // Only check user messages to match buildContentParts behavior
20
+ if (msg.role !== "user") {
21
+ return false;
22
+ }
18
23
  if (Array.isArray(msg.content)) {
19
24
  return msg.content.some((part) => typeof part === "object" &&
20
25
  part !== null &&
@@ -42,7 +47,7 @@ export async function executeVideoAnalysis(messages, options) {
42
47
  options.providerName === AIProviderName.VERTEX
43
48
  ? AIProviderName.VERTEX
44
49
  : AIProviderName.AUTO;
45
- const videoAnalysisText = await analyzeVideo(messages[0], {
50
+ const videoAnalysisText = await analyzeVideo(messages, {
46
51
  provider: provider,
47
52
  project: options.region
48
53
  ? undefined
@@ -9,9 +9,10 @@ import type { CoreMessage } from "ai";
9
9
  import { AIProviderName } from "../constants/enums.js";
10
10
  /**
11
11
  * Check if messages contain video frames (images)
12
+ * Only checks user messages to match buildContentParts behavior
12
13
  *
13
14
  * @param messages - Array of CoreMessage objects
14
- * @returns true if video frames are present
15
+ * @returns true if video frames are present in user messages
15
16
  */
16
17
  export declare function hasVideoFrames(messages: CoreMessage[]): boolean;
17
18
  /**
@@ -9,12 +9,17 @@ import { AIProviderName } from "../constants/enums.js";
9
9
  import { logger } from "./logger.js";
10
10
  /**
11
11
  * Check if messages contain video frames (images)
12
+ * Only checks user messages to match buildContentParts behavior
12
13
  *
13
14
  * @param messages - Array of CoreMessage objects
14
- * @returns true if video frames are present
15
+ * @returns true if video frames are present in user messages
15
16
  */
16
17
  export function hasVideoFrames(messages) {
17
18
  return messages.some((msg) => {
19
+ // Only check user messages to match buildContentParts behavior
20
+ if (msg.role !== "user") {
21
+ return false;
22
+ }
18
23
  if (Array.isArray(msg.content)) {
19
24
  return msg.content.some((part) => typeof part === "object" &&
20
25
  part !== null &&
@@ -42,7 +47,7 @@ export async function executeVideoAnalysis(messages, options) {
42
47
  options.providerName === AIProviderName.VERTEX
43
48
  ? AIProviderName.VERTEX
44
49
  : AIProviderName.AUTO;
45
- const videoAnalysisText = await analyzeVideo(messages[0], {
50
+ const videoAnalysisText = await analyzeVideo(messages, {
46
51
  provider: provider,
47
52
  project: options.region
48
53
  ? undefined
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.10.0",
3
+ "version": "9.10.1",
4
4
  "description": "Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and deploy AI applications with 13 providers: OpenAI, Anthropic, Google AI, AWS Bedrock, Azure, Hugging Face, Ollama, and Mistral AI.",
5
5
  "author": {
6
6
  "name": "Juspay Technologies",