@peebles-group/agentlib-js 1.0.4 → 2.0.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/README.md CHANGED
@@ -8,6 +8,10 @@ A lightweight Node.js library for building AI agents with LLM providers and MCP
8
8
  npm install @peebles-group/agentlib-js
9
9
  ```
10
10
 
11
+ ## Testing
12
+
13
+ Run `npm test` to run the test script under `tests/test.js`.
14
+
11
15
  ## Quick Start
12
16
 
13
17
  1. **Set up API keys**
@@ -40,11 +44,11 @@ import { Agent, LLMService } from '@peebles-group/agentlib-js';
40
44
  import dotenv from 'dotenv';
41
45
  dotenv.config();
42
46
 
43
- // Initialize LLM service for direct tasks
47
+ // Initialize LLM service
44
48
  const llm = new LLMService('openai', process.env.OPENAI_API_KEY);
45
49
 
46
50
  // Simple agent
47
- const agent = new Agent('openai', process.env.OPENAI_API_KEY, {
51
+ const agent = new Agent(llm, {
48
52
  model: 'gpt-4o-mini'
49
53
  });
50
54
  agent.addInput({ role: 'user', content: 'Hello!' });
@@ -52,7 +56,7 @@ const response = await agent.run();
52
56
  console.log(response.output_text);
53
57
 
54
58
  // Agent with MCP servers (auto-installs packages)
55
- const mcpAgent = new Agent('openai', process.env.OPENAI_API_KEY, {
59
+ const mcpAgent = new Agent(llm, {
56
60
  model: 'gpt-4o-mini',
57
61
  enableMCP: true
58
62
  });
@@ -64,6 +68,34 @@ await mcpAgent.addMCPServer('browser', {
64
68
  });
65
69
  ```
66
70
 
71
+ ## Prompt Management
72
+
73
+ Manage prompts efficiently using the `PromptLoader`. Support for yml/db/md/json/txt files.
74
+
75
+ ```javascript
76
+ import { PromptLoader } from '@peebles-group/agentlib-js';
77
+
78
+ // Load prompts from a file
79
+ const loader = await PromptLoader.create('./prompts.yml');
80
+
81
+ /*
82
+ prompts.yml
83
+
84
+ system_instruction: |
85
+ Write an essay on {{topic}}.
86
+ Make sure to make it {{depth}}.
87
+ */
88
+
89
+ // Get and format a prompt
90
+ const prompt = loader.getPrompt('system_instruction').format({
91
+ topic: 'AI Agents',
92
+ depth: 'detailed'
93
+ });
94
+
95
+ agent.addInput({ role: 'user', content: prompt });
96
+
97
+ ```
98
+
67
99
  ## Structured Outputs
68
100
 
69
101
  AgentLib supports type-safe structured outputs using Zod schemas for reliable JSON responses.
package/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { LLMService } from './src/llmService.js';
2
- export { Agent } from './src/Agent.js';
2
+ export { Agent } from './src/Agent.js';
3
+ export { PromptLoader } from './src/prompt-loader/promptLoader.js';
package/package.json CHANGED
@@ -1,30 +1,31 @@
1
1
  {
2
2
  "name": "@peebles-group/agentlib-js",
3
- "version": "1.0.4",
3
+ "version": "2.0.0",
4
4
  "description": "A minimal JavaScript library implementing concurrent async agents for illustrating multi-agent systems and other agentic design patterns including recursive ones purely through function calling loops.",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "scripts": {
8
8
  "start": "node index.js",
9
- "test": "echo \"Error: no test specified\" && exit 1"
9
+ "test": "cd tests && node test.js"
10
10
  },
11
11
  "author": "Peebles Group",
12
12
  "license": "MIT",
13
13
  "dependencies": {
14
14
  "@google/genai": "^1.16.0",
15
15
  "@modelcontextprotocol/sdk": "^1.18.2",
16
- "openai": "^6.0.0"
16
+ "openai": "^6.0.0",
17
+ "js-yaml": "^4.1.0",
18
+ "sqlite3": "^5.1.7",
19
+ "path": "^0.12.7"
17
20
  },
18
21
  "devDependencies": {
22
+ "@peebles-group/agentlib-js": "^2.0.0",
19
23
  "dotenv": "^16.6.1",
20
- "js-yaml": "^4.1.0",
21
24
  "mongodb": "^6.20.0",
22
25
  "openai": "^5.16.0",
23
- "@peebles-group/agentlib-js": "^1.0.0",
24
26
  "playwright": "^1.55.0",
25
27
  "prompt-sync": "^4.2.0",
26
28
  "sqlite": "^5.1.1",
27
- "sqlite3": "^5.1.7",
28
29
  "zod": "^3.25.76"
29
30
  },
30
31
  "files": [
@@ -33,4 +34,4 @@
33
34
  "README.md",
34
35
  "LICENSE"
35
36
  ]
36
- }
37
+ }
package/src/Agent.js CHANGED
@@ -1,41 +1,93 @@
1
- import { LLMService } from "./llmService.js";
2
1
  import { defaultModel } from "./config.js";
3
2
  import { MCPManager } from "./mcp/MCPManager.js";
3
+ import { PromptLoader } from "./prompt-loader/promptLoader.js";
4
+ import { fileURLToPath } from 'url';
5
+ import path, { dirname } from 'path';
4
6
 
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ const promptLoader = await PromptLoader.create(path.join(__dirname, 'prompts', 'agentPrompts.yml'));
11
+
12
+ /**
13
+ * Represents an LLM-based agent capable of tool calling.
14
+ */
5
15
  export class Agent {
6
- constructor(provider, apiKey, {model = defaultModel, tools = [], inputSchema = null, outputSchema = null, enableMCP = false, ...options} = {}) {
7
- this.llmService = new LLMService(provider, apiKey);
16
+ /**
17
+ * @param {object} llmService - The LLM service used for communication with LLM client.
18
+ * @param {object} [options] - Configuration options for the agent.
19
+ * @param {string} [options.model=defaultModel] - The model identifier to use.
20
+ * @param {Array<object>} [options.tools=[]] - Array of native tools available to the agent.
21
+ * @param {zod object|null} [options.inputSchema=null] - Zod schema for validating input messages.
22
+ * @param {zod object|null} [options.outputSchema=null] - Zod schema for expected final output format.
23
+ * @param {boolean} [options.enableMCP=false] - Whether to enable MCP (Model Context Protocol) usage.
24
+ * @param {boolean} [options.redundantToolInfo=true] - Whether to include tool descriptions in the system prompt.
25
+ * @param {object} [options...] - Additional options passed to the LLM service.
26
+ */
27
+ constructor(llmService, { model = defaultModel, tools = [], inputSchema = null, outputSchema = null, enableMCP = false, redundantToolInfo = true, ...options } = {}) {
28
+ this.llmService = llmService;
8
29
  this.model = model;
9
30
  this.nativeTools = tools;
10
31
  this.inputSchema = inputSchema;
11
32
  this.outputSchema = outputSchema;
12
33
  this.mcpManager = enableMCP ? new MCPManager() : null;
13
- this.updateSystemPrompt();
14
- this.options = options;
34
+ this.redundantToolInfo = redundantToolInfo;
35
+ this.additionalOptions = options;
36
+ this.input = [];
37
+
38
+ if (this.redundantToolInfo) {
39
+ this.updateSystemPrompt();
40
+ }
15
41
  }
16
42
 
43
+ /**
44
+ * Adds a new MCP server for remote tool access.
45
+ * @param {string} serverName - A unique name for the server.
46
+ * @param {object} config - Configuration details for the MCP server connection.
47
+ * @returns {Promise<object>} The result of adding the server.
48
+ * @throws {Error} If MCP is not enabled.
49
+ */
17
50
  async addMCPServer(serverName, config) {
18
51
  if (!this.mcpManager) {
19
52
  throw new Error("MCP is not enabled for this agent");
20
- }
53
+ }
21
54
 
22
55
  const result = await this.mcpManager.addServer(serverName, config);
23
- this.updateSystemPrompt();
56
+ if (this.redundantToolInfo) {
57
+ this.updateSystemPrompt();
58
+ }
24
59
  return result;
25
60
  }
26
61
 
62
+ /**
63
+ * Removes an existing MCP server.
64
+ * @param {string} serverName - The unique name of the server to remove.
65
+ * @returns {Promise<boolean>} True if the server was successfully removed, false otherwise.
66
+ */
27
67
  async removeMCPServer(serverName) {
28
68
  if (!this.mcpManager) return false;
29
69
 
30
70
  const result = await this.mcpManager.removeServer(serverName);
31
- if (result) this.updateSystemPrompt();
71
+ if (result && this.redundantToolInfo) {
72
+ this.updateSystemPrompt();
73
+ }
32
74
  return result;
33
75
  }
34
76
 
77
+ /**
78
+ * Adds a native tool to the agent's array of tools.
79
+ * @param {object} tool - The tool object.
80
+ * @param {string} tool.name - The unique name of the tool.
81
+ * @param {function} tool.func - The function to execute when the tool is called.
82
+ * @param {string} [tool.description=''] - A description for the LLM on when to use the tool.
83
+ * @returns {object} The added tool object.
84
+ * @throws {Error} If the tool is invalid or a name collision occurs.
85
+ */
35
86
  addTool(tool) {
36
87
  if (!tool || typeof tool !== 'object') {
37
- throw new Error("Invalid tool: expected an object");
88
+ throw new Error('Invalid tool: expected an object');
38
89
  }
90
+
39
91
  const { name, func } = tool;
40
92
  if (typeof name !== 'string' || name.trim() === '') {
41
93
  throw new Error("Invalid tool: missing valid 'name' (string)");
@@ -44,7 +96,6 @@ export class Agent {
44
96
  throw new Error("Invalid tool: missing 'func' (function)");
45
97
  }
46
98
 
47
- // Prevent name collisions across native and MCP tools
48
99
  const nameExistsInNative = this.nativeTools.some(t => t && t.name === name);
49
100
  const nameExistsInMCP = this.mcpManager ? this.mcpManager.getAllTools().some(t => t && t.name === name) : false;
50
101
  if (nameExistsInNative || nameExistsInMCP) {
@@ -56,30 +107,45 @@ export class Agent {
56
107
  }
57
108
 
58
109
  this.nativeTools.push(tool);
59
- this.updateSystemPrompt();
110
+ if (this.redundantToolInfo) {
111
+ this.updateSystemPrompt();
112
+ }
60
113
  return tool;
61
114
  }
62
115
 
116
+ /**
117
+ * Retrieves all available tools, including native and MCP tools.
118
+ * @returns {Array<object>} An array of all tools.
119
+ */
63
120
  getAllTools() {
64
121
  const mcpTools = this.mcpManager ? this.mcpManager.getAllTools() : [];
65
122
  return [...this.nativeTools, ...mcpTools];
66
123
  }
67
124
 
125
+ /**
126
+ * Gets status information about the MCP manager.
127
+ * @returns {object} Information about the MCP manager, or { enabled: false } if disabled.
128
+ */
68
129
  getMCPInfo() {
69
130
  return this.mcpManager ? this.mcpManager.getServerInfo() : { enabled: false };
70
131
  }
71
132
 
72
- // Mentioning the tools in the system prompt for maximum reliability
133
+ /**
134
+ * Updates the system prompt with descriptions of all currently available tools.
135
+ */
73
136
  updateSystemPrompt() {
74
137
  const allTools = this.getAllTools();
138
+ const toolDescriptions = allTools.map(tool => `${tool.name}: ${tool.description}`).join('; ');
75
139
  this.input = [{
76
140
  role: 'system',
77
- content: 'You are a tool-calling agent. You have access to the following tools: ' +
78
- allTools.map(tool => `${tool.name}: ${tool.description}`).join('; ') +
79
- '. Use these tools to answer the user\'s questions.'
141
+ content: promptLoader.getPrompt('systemPrompt').format({ toolDescriptions })
80
142
  }];
81
143
  }
82
144
 
145
+ /**
146
+ * Adds user instruction or assistant response to the current conversation history.
147
+ * @param {object} input - The message object to add.
148
+ */
83
149
  addInput(input) {
84
150
  if (this.inputSchema) {
85
151
  this.inputSchema.parse(input);
@@ -88,26 +154,25 @@ export class Agent {
88
154
  }
89
155
 
90
156
  /**
91
- * Run the agent for a single step
157
+ * Runs the agent for a single conversational turn, including tool use if necessary.
158
+ * This method handles the multi-step reasoning: LLM -> Tool Execution -> LLM Final Response.
159
+ * @returns {Promise<object>} The final response object from the LLM, including execution details.
92
160
  */
93
161
  async run() {
94
162
  const allTools = this.getAllTools();
95
163
  const executed = []
96
164
 
97
- // Step 1: send input to model
98
165
  let response = await this.llmService.chat(this.input, {
99
166
  model: this.model,
100
167
  outputSchema: this.outputSchema,
101
168
  tools: allTools,
102
- ...this.options,
169
+ ...this.additionalOptions
103
170
  });
104
171
 
105
172
  const { output, rawResponse } = response;
106
173
 
107
- // Step 2: Clean and add the response to input history
108
174
  rawResponse.output.forEach(item => {
109
175
  if (item.type === "function_call") {
110
- // Remove parsed_arguments if it exists
111
176
  const { parsed_arguments, ...rest } = item;
112
177
  const cleanedItem = { ...rest, arguments: JSON.stringify(item.arguments) };
113
178
  this.addInput(cleanedItem);
@@ -116,7 +181,6 @@ export class Agent {
116
181
  }
117
182
  });
118
183
 
119
- // Step 3: collect all function calls
120
184
  const functionCalls = rawResponse.output.filter(item => item.type === "function_call");
121
185
 
122
186
  if (functionCalls.length > 0) {
@@ -131,10 +195,8 @@ export class Agent {
131
195
  throw new Error(`Tool ${call.name} not found or missing implementation.`);
132
196
  }
133
197
 
134
- // Step 4: execute the function
135
198
  const result = await tool.func(args);
136
199
 
137
- // Step 5: append function call output to input
138
200
  this.input.push({
139
201
  type: "function_call_output",
140
202
  call_id: call.call_id,
@@ -147,15 +209,20 @@ export class Agent {
147
209
  tools: allTools,
148
210
  model: this.model,
149
211
  outputSchema: this.outputSchema,
212
+ ...this.additionalOptions
150
213
  });
151
214
  }
152
215
  response.executed = executed;
153
216
  return response;
154
217
  }
155
218
 
219
+ /**
220
+ * Performs cleanup operations, primarily closing MCP server connections.
221
+ * @returns {Promise<void>}
222
+ */
156
223
  async cleanup() {
157
224
  if (this.mcpManager) {
158
225
  await this.mcpManager.cleanup();
159
226
  }
160
227
  }
161
- }
228
+ }
package/src/config.js CHANGED
@@ -1 +1,2 @@
1
+ export const defaultProvider = 'openai';
1
2
  export const defaultModel = 'gpt-5';
package/src/llmService.js CHANGED
@@ -1,30 +1,25 @@
1
- import { validateProviderName } from './providers/registry.js';
1
+ import { getAllowedProviders, validateProviderName } from './providers/registry.js';
2
2
 
3
3
  export class LLMService {
4
4
  constructor(provider, apiKey) {
5
5
  this.provider = validateProviderName(provider);
6
+ this.providerNamespace = getAllowedProviders()[this.provider]?.namespace;
6
7
  this.apiKey = apiKey;
7
- this.client = null;
8
+ this.client = this._getProviderClient();
8
9
 
9
10
  if (!apiKey) {
10
11
  throw new Error(`API key is required for provider: ${provider}`);
11
12
  }
12
13
  }
13
14
 
14
- async _getProviderClient() {
15
-
16
- if (!this.client) {
17
- const provider = await import(`./providers/${this.provider}.js`);
18
- this.client = provider.createClient(this.apiKey);
19
- }
20
- return this.client;
15
+ // Instead of using a dynamic import here, we use the imported registry namespace
16
+ _getProviderClient() {
17
+ // Returns the client instance for the specified provider
18
+ return this.providerNamespace.createClient(this.apiKey);
21
19
  }
22
20
 
23
- async chat(input, {inputSchema = null, outputSchema = null, ...options} = {}) {
24
- const client = await this._getProviderClient();
25
- const provider = await import(`./providers/${this.provider}.js`);
26
-
27
- return provider.chat(client, input, {
21
+ async chat(input, {inputSchema = null, outputSchema = null, ...options} = {}) {
22
+ return this.providerNamespace.chat(this.client, input, {
28
23
  inputSchema,
29
24
  outputSchema,
30
25
  ...options
@@ -0,0 +1,63 @@
1
+ import { promises as fs } from 'fs';
2
+ import http from 'http';
3
+ import https from 'https';
4
+ import sqlite3 from 'sqlite3';
5
+
6
+ export const loadStrategies = {
7
+ /**
8
+ * Loads and parses a file from the local file system.
9
+ * @param {string} path - The file path.
10
+ * @returns {Promise<string>} The file contents.
11
+ */
12
+ file: async (path) => {
13
+ try {
14
+ const fileContents = await fs.readFile(path, 'utf-8');
15
+ return fileContents;
16
+ } catch (error) {
17
+ throw new Error(`File loading error: ${error.message}`);
18
+ }
19
+ },
20
+
21
+ /**
22
+ * Loads and parses data from a URL.
23
+ * @param {string} path - The URL.
24
+ * @param {string} parserName - The key of the parser (e.g., 'json', 'yaml').
25
+ * @returns {Promise<string>} The URL contents.
26
+ */
27
+ url: (path) => {
28
+ const protocol = path.startsWith('https') ? https : http;
29
+
30
+ return new Promise((resolve, reject) => {
31
+ protocol.get(path, (response) => {
32
+ let data = '';
33
+ response.on('data', (chunk) => data += chunk);
34
+ response.on('end', () => {
35
+ try {
36
+ // Use the specified parser
37
+ resolve(data);
38
+ } catch (error) {
39
+ reject(error); // Pass up parsing error
40
+ }
41
+ });
42
+ }).on('error', (error) => {
43
+ reject(new Error(`URL loading error: ${error.message}`));
44
+ });
45
+ });
46
+ },
47
+
48
+ /**
49
+ * Loads prompt data from an SQLite database.
50
+ * @param {string} path - The path to the SQLite DB file.
51
+ * @returns {Promise<object>} The prompt data object in key-value pairs of signature: { prompt: string, output: string }.
52
+ */
53
+ sqlite: (path) => {
54
+ return new Promise((resolve, reject) => {
55
+ const db = new sqlite3.Database(path, sqlite3.OPEN_READONLY, (err) => {
56
+ if (err) {
57
+ return reject(new Error(`SQLite connection error: ${err.message}`));
58
+ }
59
+ });
60
+ resolve(db);
61
+ });
62
+ }
63
+ };
@@ -0,0 +1,79 @@
1
+ import yaml from 'js-yaml';
2
+
3
+ export const parseStrategies = {
4
+ /**
5
+ * Parses a YAML string.
6
+ * @param {string} data - The raw YAML string.
7
+ * @returns {object} The parsed JavaScript object.
8
+ */
9
+ yaml: (data) => {
10
+ try {
11
+ return yaml.load(data);
12
+ } catch (error) {
13
+ throw new Error(`YAML parsing error: ${error.message}`);
14
+ }
15
+ },
16
+
17
+ /**
18
+ * Parses a JSON string.
19
+ * @param {string} data - The raw JSON string.
20
+ * @returns {object} The parsed JavaScript object.
21
+ */
22
+ json: (data) => {
23
+ try {
24
+ return JSON.parse(data);
25
+ } catch (error) {
26
+ throw new Error(`JSON parsing error: ${error.message}`);
27
+ }
28
+ },
29
+
30
+ /**
31
+ * Parses a sqlite database.
32
+ * @param {object} db - The sqlite database.
33
+ * @returns {object} The parsed sqlite database in key-value pairs of signature: { prompt: string, output: string }.
34
+ */
35
+ sqlite: (db) => {
36
+ return new Promise((resolve, reject) => {
37
+ const query = 'SELECT signature, prompt, output FROM prompt_store';
38
+ db.all(query, [], (err, rows) => {
39
+ if (err) {
40
+ return reject(new Error(`SQLite query error: ${err.message}`));
41
+ }
42
+
43
+ const data = {};
44
+ rows.forEach(row => {
45
+ data[row.signature] = { prompt: row.prompt, output: row.output };
46
+ });
47
+
48
+ db.close((err) => {
49
+ if (err) {
50
+ console.error(`SQLite close error: ${err.message}`);
51
+ }
52
+ });
53
+ resolve(data);
54
+ });
55
+ });
56
+ },
57
+
58
+ /**
59
+ * Parses a custom text format.
60
+ * Sections are delimited by a delimiter at the start of a line (default is '#').
61
+ * The first line of a section is the key.
62
+ * @param {string} data - The raw text data.
63
+ * @param {string} delimiter - The delimiter at the start of a line (default is '#').
64
+ * @returns {object} A dictionary of sections.
65
+ */
66
+ customText: (data, delimiter = '#') => {
67
+ // Use RegExp to split only on delimiters at the beginning of a line
68
+ const sections = data.split(new RegExp(`^${delimiter}`, 'm'));
69
+ const sectionDict = sections.reduce((acc, section) => {
70
+ const lines = section.trim().split('\n');
71
+ const key = lines.shift();
72
+ if (key) {
73
+ acc[key.trim()] = lines.join('\n');
74
+ }
75
+ return acc;
76
+ }, {});
77
+ return sectionDict;
78
+ }
79
+ };
@@ -0,0 +1,188 @@
1
+ import { loadStrategies } from './loadStrategies.js';
2
+ import { parseStrategies } from './parseStrategies.js';
3
+ import path from 'path';
4
+
5
+ class Prompt {
6
+ /**
7
+ * @param {string} templateString The raw prompt string.
8
+ * @param {string} startDel The start delimiter, e.g., '{{'
9
+ * @param {string} endDel The end delimiter, e.g., '}}'
10
+ */
11
+ constructor(templateString, startDel = '{{', endDel = '}}') {
12
+ this.template = templateString;
13
+ this.delimiterStart = startDel;
14
+ this.delimiterEnd = endDel;
15
+
16
+ // Escape special chars so user delimiters like '?' don't break regex logic.
17
+ const escapedStart = this.delimiterStart.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
18
+ const escapedEnd = this.delimiterEnd.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
19
+
20
+ // Matches start delimiter, captures variable name (alphanumeric + dots), matches end.
21
+ this.varRegex = new RegExp(`${escapedStart}\\s*([a-zA-Z0-9_.]+)\\s*${escapedEnd}`, 'g');
22
+
23
+ this.variables = this._discoverVariables();
24
+ }
25
+
26
+ /**
27
+ * Discovers all unique variables in the template.
28
+ * @returns {Set<string>} A set of variable names.
29
+ */
30
+ _discoverVariables() {
31
+ const vars = new Set();
32
+ let match;
33
+ this.varRegex.lastIndex = 0;
34
+ while ((match = this.varRegex.exec(this.template)) !== null) {
35
+ vars.add(match[1].trim());
36
+ }
37
+ this.varRegex.lastIndex = 0;
38
+ return vars;
39
+ }
40
+
41
+ /**
42
+ * Formats the prompt template with the given variables.
43
+ * @param {object} variables - An object where keys are variable names.
44
+ * @returns {string} The formatted prompt string.
45
+ */
46
+ format(variables = {}) {
47
+ return this.template.replace(this.varRegex, (match, varPath) => {
48
+ const cleanPath = varPath.trim();
49
+ const value = variables[cleanPath];
50
+
51
+ // If value is undefined, leave the variable intact
52
+ return value !== undefined ? String(value) : match;
53
+ });
54
+ }
55
+
56
+ /**
57
+ * @returns {string[]} An array of variable names found in the prompt.
58
+ */
59
+ getVars() {
60
+ return Array.from(this.variables);
61
+ }
62
+ }
63
+
64
+
65
+ // --- Main Prompt Loader Class ---
66
+
67
+ /**
68
+ * Handles loading prompts from various sources and provides
69
+ * access to formatted Prompt objects.
70
+ * * Use the static `PromptLoader.create()` method to instantiate.
71
+ */
72
+ class PromptLoader {
73
+ /**
74
+ * Private constructor. Use `PromptLoader.create()` to instantiate.
75
+ * @param {object} promptData - The raw, parsed object (e.g., { "greeting": "..." })
76
+ * @param {object} [options] - Options passed from create, including delimiter settings.
77
+ */
78
+ constructor(promptData, options = {}) {
79
+ this.prompts = new Map();
80
+
81
+ const startDel = options.delimiterStart || '{{';
82
+ const endDel = options.delimiterEnd || '}}';
83
+
84
+ for (const [key, value] of Object.entries(promptData)) {
85
+ let promptString;
86
+
87
+ // Handle different data structures
88
+ if (typeof value === 'string') {
89
+ // E.g., { "greeting": "Hello {{name}}" }
90
+ promptString = value;
91
+ } else if (value && typeof value.prompt === 'string') {
92
+ // E.g., { "greeting": { "prompt": "Hello {{name}}", "output": "..." } }
93
+ promptString = value.prompt;
94
+ }
95
+
96
+ if (promptString) {
97
+ // Pass custom delimiters to the Prompt constructor
98
+ this.prompts.set(key, new Prompt(promptString, startDel, endDel));
99
+ } else {
100
+ console.warn(`No valid prompt string found for key: ${key}`);
101
+ }
102
+ }
103
+ }
104
+
105
+ static _determineLoader(resourcePathOrUrl, parserName) {
106
+ // SQLite requires a specific loader (db connection), regardless of path
107
+ if (parserName === 'sqlite') return 'sqlite';
108
+
109
+ // Otherwise, check protocol
110
+ const isUrl = resourcePathOrUrl.startsWith('http://') || resourcePathOrUrl.startsWith('https://');
111
+ return isUrl ? 'url' : 'file';
112
+ }
113
+
114
+ /**
115
+ * Determines the parser name based on the resource path.
116
+ * @param {string} resourcePath - The path or URL of the resource.
117
+ * @returns {string} The name of the parser strategy ('yaml', 'json', 'sqlite', or 'customText').
118
+ */
119
+ static _determineParser(resourcePath) {
120
+ const cleanPath = resourcePath.split('?')[0];
121
+ const extension = path.extname(cleanPath).toLowerCase();
122
+
123
+ const map = {
124
+ '.yaml': 'yaml',
125
+ '.yml': 'yaml',
126
+ '.json': 'json',
127
+ '.db': 'sqlite',
128
+ '.sqlite': 'sqlite',
129
+ '.txt': 'customText',
130
+ '.md': 'customText'
131
+ };
132
+
133
+ return map[extension] || 'customText';
134
+ }
135
+
136
+ /**
137
+ * Asynchronously creates and initializes a PromptLoader.
138
+ * This is the main entry point for the class.
139
+ * @param {string} resourcePathOrUrl - The file path or URL to the prompt resource.
140
+ * @param {object} [options] - Optional settings.
141
+ * @param {string} [options.parser] - Explicitly set the parser (overrides extension analysis).
142
+ * @returns {Promise<PromptLoader>} A new, initialized PromptLoader instance.
143
+ */
144
+ static async create(resourcePathOrUrl, options = {}) {
145
+ // 1. Determine Strategies
146
+ const parserName = options.parser || PromptLoader._determineParser(resourcePathOrUrl);
147
+ const loaderName = PromptLoader._determineLoader(resourcePathOrUrl, parserName);
148
+
149
+ // 2. Validate
150
+ if (!loadStrategies[loaderName]) throw new Error(`Unknown loader: ${loaderName}`);
151
+ if (!parseStrategies[parserName]) throw new Error(`Unknown parser: ${parserName}`);
152
+
153
+ // 3. Universal Execution Flow
154
+ // 'rawResource' adapts: it's a String for files, or a DbConnection for sqlite
155
+ const rawResource = await loadStrategies[loaderName](resourcePathOrUrl);
156
+
157
+ // The parser strategy handles its specific input type (String or DbConnection)
158
+ const promptData = await parseStrategies[parserName](rawResource);
159
+
160
+ return new PromptLoader(promptData, options);
161
+ }
162
+
163
+ /**
164
+ * Retrieves an initialized Prompt object by its ID.
165
+ * @param {string} id - The key of the prompt.
166
+ * @returns {Prompt | undefined} The Prompt object, or undefined if not found.
167
+ */
168
+ getPrompt(id) {
169
+ const prompt = this.prompts.get(id);
170
+ if (!prompt) {
171
+ console.error(`Prompt ID "${id}" does not exist.`);
172
+ return undefined;
173
+ }
174
+ return prompt;
175
+ }
176
+
177
+ /**
178
+ * @returns {Map<string, Prompt>} A map of all loaded prompt objects.
179
+ */
180
+ getAllPrompts() {
181
+ return this.prompts;
182
+ }
183
+ }
184
+
185
+ export {
186
+ PromptLoader,
187
+ Prompt,
188
+ };
@@ -0,0 +1,9 @@
1
+ {
2
+ "greeting_simple": "Hello, {{user_name}}! Welcome to the system.",
3
+ "analyzer_complex": "Review the following text and perform a {{task_type}} analysis.\nFocus on {{metric_1}} and {{metric_2}}.\n\n***TEXT TO ANALYZE***\n{{raw_text_input}}\n***END OF TEXT***\n\nProvide your output as a JSON object.",
4
+ "summarizer_with_metadata": {
5
+ "prompt": "Summarize the document titled '{{document_title}}' into a maximum of {{word_count}} words.",
6
+ "output": "plain_text",
7
+ "version": 2.1
8
+ }
9
+ }
@@ -0,0 +1,12 @@
1
+ # greeting_simple
2
+ Hello, {{user_name}}! Welcome to the system.
3
+
4
+ # analyzer_complex
5
+ Review the following text and perform a {{task_type}} analysis.
6
+ Focus on {{metric_1}} and {{metric_2}}.
7
+
8
+ ***TEXT TO ANALYZE***
9
+ {{raw_text_input}}
10
+ ***END OF TEXT***
11
+
12
+ Provide your output as a JSON object.
@@ -0,0 +1,12 @@
1
+ # greeting_simple
2
+ Hello, {{user_name}}! Welcome to the system.
3
+
4
+ # analyzer_complex
5
+ Review the following text and perform a {{task_type}} analysis.
6
+ Focus on {{metric_1}} and {{metric_2}}.
7
+
8
+ ***TEXT TO ANALYZE***
9
+ {{raw_text_input}}
10
+ ***END OF TEXT***
11
+
12
+ Provide your output as a JSON object.
@@ -0,0 +1,16 @@
1
+ greeting_simple: "Hello, {{user_name}}! Welcome to the system."
2
+
3
+ analyzer_complex: |
4
+ Review the following text and perform a {{task_type}} analysis.
5
+ Focus on {{metric_1}} and {{metric_2}}.
6
+
7
+ ***TEXT TO ANALYZE***
8
+ {{raw_text_input}}
9
+ ***END OF TEXT***
10
+
11
+ Provide your output as a JSON object.
12
+
13
+ summarizer_with_metadata:
14
+ prompt: "Summarize the document titled '{{document_title}}' into a maximum of {{word_count}} words."
15
+ output: "plain_text"
16
+ version: 2.1
@@ -0,0 +1,151 @@
1
+ import {
2
+ PromptLoader
3
+ } from '../promptLoader.js';
4
+ import { LLMService } from '@peebles-group/agentlib-js';
5
+ import dotenv from 'dotenv';
6
+ dotenv.config({ path: '../../../.env' });
7
+
8
+ const llm = new LLMService('openai', process.env.OPENAI_API_KEY);
9
+
10
+ const DB_FILE = './test-prompts.db';
11
+ const YAML_FILE = './test-prompts.yaml';
12
+ const JSON_FILE = './test-prompts.json';
13
+ const TXT_FILE = './test-prompts.txt';
14
+ const MD_FILE = './test-prompts.md';
15
+
16
+ const COMMON_VARIABLES = {
17
+ user_name: 'Arsen',
18
+ current_date: 'Monday'
19
+ };
20
+
21
+ const FILE_EXPECTED = "Hello, Arsen! Welcome to the system.";
22
+
23
+ const DB_EXPECTED = "Hello, Arsen! Welcome to the system. Today is Monday.";
24
+
25
+ const TEST_MAP = [
26
+ {
27
+ name: "YAML Test (.yaml)",
28
+ path: YAML_FILE,
29
+ signature: "greeting_simple",
30
+ variables: COMMON_VARIABLES,
31
+ expected: FILE_EXPECTED
32
+ },
33
+ {
34
+ name: "JSON Test (.json)",
35
+ path: JSON_FILE,
36
+ signature: "greeting_simple",
37
+ variables: COMMON_VARIABLES,
38
+ expected: FILE_EXPECTED
39
+ },
40
+ {
41
+ name: "Text Test (.txt)",
42
+ path: TXT_FILE,
43
+ signature: "greeting_simple",
44
+ variables: COMMON_VARIABLES,
45
+ expected: FILE_EXPECTED
46
+ },
47
+ {
48
+ name: "Markdown Test (.md)",
49
+ path: MD_FILE,
50
+ signature: "greeting_simple",
51
+ variables: COMMON_VARIABLES,
52
+ expected: FILE_EXPECTED
53
+ },
54
+ {
55
+ name: "SQLite Test (.db)",
56
+ path: DB_FILE,
57
+ signature: "greeting_simple",
58
+ variables: COMMON_VARIABLES,
59
+ expected: DB_EXPECTED
60
+ },
61
+ ];
62
+
63
+ async function runTest(testName, resourcePath, promptSignature, variables, expectedOutput) {
64
+ let loader;
65
+ console.log(`\n--- Running Test: ${testName} ---`);
66
+ console.log(` Resource Path: ${resourcePath}`);
67
+ try {
68
+ loader = await PromptLoader.create(resourcePath);
69
+ const prompt = loader.getPrompt(promptSignature);
70
+
71
+ if (!prompt) {
72
+ console.error(`[FAIL]: Prompt signature "${promptSignature}" not found.`);
73
+ const availableKeys = Array.from(loader.getAllPrompts().keys());
74
+ console.log(` [WARNING]: Available keys found in file: [${availableKeys.join(', ')}]`);
75
+ return false;
76
+ }
77
+
78
+ const formatted = prompt.format(variables)
79
+ .replace(/\s+/g, ' ')
80
+ .trim();
81
+
82
+ const cleanExpected = expectedOutput.replace(/\s+/g, ' ').trim();
83
+
84
+ if (formatted === cleanExpected) {
85
+ console.log(`[PASS]: Prompt loaded and formatted correctly.`);
86
+ return true;
87
+ } else {
88
+ console.error(`[FAIL]: Output Mismatch.`);
89
+ console.error(` Expected: "${cleanExpected}"`);
90
+ console.error(` Received: "${formatted}"`);
91
+ return false;
92
+ }
93
+
94
+ } catch (error) {
95
+ console.error(`[FATAL FAIL]: Error during ${testName}: ${error.message}`);
96
+ return false;
97
+ }
98
+ }
99
+
100
+ async function main() {
101
+ console.log("Starting PromptLoader Versatility Test (Custom Delimiter mode)");
102
+
103
+ let allPassed = true;
104
+
105
+ for (const testCase of TEST_MAP) {
106
+ const passed = await runTest(
107
+ testCase.name,
108
+ testCase.path,
109
+ testCase.signature,
110
+ testCase.variables,
111
+ testCase.expected
112
+ );
113
+ allPassed = allPassed && passed;
114
+ }
115
+
116
+ console.log("\n----------------------------------------------------");
117
+ console.log(allPassed ? "[SUCCESS]: ALL TESTS PASSED SUCCESSFULLY!" : "[FAILURE]: ONE OR MORE TESTS FAILED. CHECK LOGS ABOVE.");
118
+ console.log("----------------------------------------------------");
119
+
120
+ // --- LLM Service Test ---
121
+ console.log("\n--- Starting LLM Service Test ---");
122
+
123
+ const LLM_TEST_SIGNATURE = 'analyzer_complex';
124
+ const LLM_TEST_PATH = YAML_FILE;
125
+
126
+ try {
127
+ const loader = await PromptLoader.create(LLM_TEST_PATH);
128
+ const prompt = loader.getPrompt(LLM_TEST_SIGNATURE);
129
+
130
+ if (!prompt) {
131
+ console.error(`[LLM TEST FAIL]: Prompt signature '${LLM_TEST_SIGNATURE}' not found in ${LLM_TEST_PATH}. Skipping LLM call.`);
132
+ return;
133
+ }
134
+
135
+ const formattedPrompt = prompt.format(COMMON_VARIABLES);
136
+
137
+ const input = [{ role: 'user', content: formattedPrompt }];
138
+
139
+ console.log(`[INFO]: Sending prompt to LLM: "${formattedPrompt}"`);
140
+
141
+ const response = await llm.chat(input, { model: 'gpt-4o-mini' });
142
+
143
+ console.log(`\n[LLM RESPONSE]:`);
144
+ console.log(response);
145
+
146
+ } catch (error) {
147
+ console.error(`[LLM TEST FAIL]: Failed to execute LLM call.`, error.message);
148
+ }
149
+ }
150
+
151
+ main();
@@ -0,0 +1,3 @@
1
+ systemPrompt: |
2
+ You are a tool-calling agent. You have access to the following tools: {{toolDescriptions}}.
3
+ Use these tools to answer the user's questions.
@@ -36,7 +36,7 @@ export async function chat(client, input, { inputSchema, outputSchema, ...option
36
36
  }
37
37
  return {output: output, rawResponse: response};
38
38
  } catch (error) {
39
- console.error(`Error during OpenAI chat completion:`, error);
39
+ console.error(`Error during OpenAI chat response creation:`, error);
40
40
  throw error;
41
41
  }
42
42
  }
@@ -1,18 +1,31 @@
1
- const ALLOWED_PROVIDERS = Object.freeze(['openai', 'gemini']);
1
+ import * as OpenAIProvider from './openai.js';
2
+ import * as GeminiProvider from './gemini.js';
3
+ // Need to import namespaces when adding new providers here
4
+
5
+ const ALLOWED_PROVIDERS = {
6
+ openai: {name: 'OpenAI', namespace: OpenAIProvider},
7
+ gemini: {name: 'Gemini', namespace: GeminiProvider},
8
+ };
9
+ // Need to add new providers to this object in this format
2
10
 
3
11
  export function getAllowedProviders() {
4
- return [...ALLOWED_PROVIDERS];
12
+ // Procedure that returns the object
13
+ return ALLOWED_PROVIDERS;
5
14
  }
6
15
 
7
16
  export function validateProviderName(providerName) {
17
+ // Checks if a valid provider name has been passed and returns normalized name
8
18
  if (typeof providerName !== 'string') {
9
19
  throw new TypeError('Provider name must be a string.');
10
20
  }
11
21
 
12
- const normalizedName = providerName.trim().toLowerCase();
22
+ const normalize = text => text.trim().toLowerCase();
23
+
24
+ const allowedProviders = Object.values(getAllowedProviders()).map(provider => normalize(provider.name));
25
+ const normalizedName = normalize(providerName); // this part is ok
13
26
 
14
- if (!ALLOWED_PROVIDERS.includes(normalizedName)) {
15
- throw new Error(`Unsupported provider. Allowed providers: ${ALLOWED_PROVIDERS.join(', ')}`);
27
+ if (!allowedProviders.includes(normalizedName)) {
28
+ throw new Error(`Unsupported provider. Allowed providers: ${allowedProviders.join(', ')}`);
16
29
  }
17
30
 
18
31
  return normalizedName;