From 908a2e248e38a1745ebd2aa458eccc05d9908b2e Mon Sep 17 00:00:00 2001 From: shelabastida933-pixel <308737702+shelabastida933-pixel@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:30:07 +0800 Subject: [PATCH 1/2] Add Palm2 AI provider --- JS/edgechains/arakoodev/src/ai/src/index.ts | 1 + .../arakoodev/src/ai/src/lib/palm2/palm2.ts | 95 +++++++++++++++++++ .../src/ai/src/tests/palm2/palm2.test.ts | 86 +++++++++++++++++ .../chat-with-palm2/jsonnet/main.jsonnet | 15 +++ .../chat-with-palm2/jsonnet/secrets.jsonnet | 3 + .../examples/chat-with-palm2/package.json | 18 ++++ .../examples/chat-with-palm2/src/index.ts | 30 ++++++ .../src/lib/generateResponse.cts | 16 ++++ .../examples/chat-with-palm2/tsconfig.json | 12 +++ 9 files changed, 276 insertions(+) create mode 100644 JS/edgechains/arakoodev/src/ai/src/lib/palm2/palm2.ts create mode 100644 JS/edgechains/arakoodev/src/ai/src/tests/palm2/palm2.test.ts create mode 100644 JS/edgechains/examples/chat-with-palm2/jsonnet/main.jsonnet create mode 100644 JS/edgechains/examples/chat-with-palm2/jsonnet/secrets.jsonnet create mode 100644 JS/edgechains/examples/chat-with-palm2/package.json create mode 100644 JS/edgechains/examples/chat-with-palm2/src/index.ts create mode 100644 JS/edgechains/examples/chat-with-palm2/src/lib/generateResponse.cts create mode 100644 JS/edgechains/examples/chat-with-palm2/tsconfig.json diff --git a/JS/edgechains/arakoodev/src/ai/src/index.ts b/JS/edgechains/arakoodev/src/ai/src/index.ts index 2c98f37dc..d2ccf0eb0 100644 --- a/JS/edgechains/arakoodev/src/ai/src/index.ts +++ b/JS/edgechains/arakoodev/src/ai/src/index.ts @@ -1,5 +1,6 @@ export { OpenAI } from "./lib/openai/openai.js"; export { GeminiAI } from "./lib/gemini/gemini.js"; +export { Palm2AI } from "./lib/palm2/palm2.js"; export { LlamaAI } from "./lib/llama/llama.js"; export { RetellAI } from "./lib/retell-ai/retell.js"; export { RetellWebClient } from "./lib/retell-ai/retellWebClient.js"; diff --git a/JS/edgechains/arakoodev/src/ai/src/lib/palm2/palm2.ts b/JS/edgechains/arakoodev/src/ai/src/lib/palm2/palm2.ts new file mode 100644 index 000000000..5313ee598 --- /dev/null +++ b/JS/edgechains/arakoodev/src/ai/src/lib/palm2/palm2.ts @@ -0,0 +1,95 @@ +import axios from "axios"; +import { retry } from "@lifeomic/attempt"; + +const DEFAULT_MODEL = "text-bison-001"; +const DEFAULT_API_VERSION = "v1beta3"; +const GOOGLE_GENERATIVE_LANGUAGE_BASE_URL = "https://generativelanguage.googleapis.com"; + +export interface Palm2AIConstructionOptions { + apiKey?: string; + model?: string; + apiVersion?: string; +} + +export interface Palm2AIChatOptions { + model?: string; + prompt: string; + temperature?: number; + candidate_count?: number; + max_output_tokens?: number; + top_k?: number; + top_p?: number; + max_retry?: number; + delay?: number; +} + +export type Palm2SafetyRating = { + category: string; + probability: string; +}; + +export type Palm2Candidate = { + output: string; + safetyRatings?: Palm2SafetyRating[]; +}; + +export type Palm2Response = { + candidates: Palm2Candidate[]; + filters?: Array<{ + reason: string; + message?: string; + }>; +}; + +export class Palm2AI { + apiKey: string; + model: string; + apiVersion: string; + + constructor(options: Palm2AIConstructionOptions = {}) { + this.apiKey = options.apiKey || process.env.PALM2_API_KEY || process.env.GOOGLE_API_KEY || ""; + this.model = options.model || DEFAULT_MODEL; + this.apiVersion = options.apiVersion || DEFAULT_API_VERSION; + this.checkKeys(); + } + + private checkKeys(): void { + if (!this.apiKey) { + console.error( + "API key is missing. Please provide a valid Google Generative Language API key. You can add it in .env file as PALM2_API_KEY or GOOGLE_API_KEY" + ); + } + } + + async chat(chatOptions: Palm2AIChatOptions): Promise { + const model = chatOptions.model || this.model; + const url = `${GOOGLE_GENERATIVE_LANGUAGE_BASE_URL}/${this.apiVersion}/models/${model}:generateText?key=${this.apiKey}`; + const data = { + prompt: { + text: chatOptions.prompt, + }, + temperature: chatOptions.temperature ?? 0.7, + candidate_count: chatOptions.candidate_count ?? 1, + max_output_tokens: chatOptions.max_output_tokens ?? 1024, + top_k: chatOptions.top_k, + top_p: chatOptions.top_p, + }; + + const config = { + method: "post", + maxBodyLength: Infinity, + url, + headers: { + "Content-Type": "application/json", + }, + data, + }; + + return await retry( + async () => { + return (await axios.request(config)).data; + }, + { maxAttempts: chatOptions.max_retry || 3, delay: chatOptions.delay || 200 } + ); + } +} diff --git a/JS/edgechains/arakoodev/src/ai/src/tests/palm2/palm2.test.ts b/JS/edgechains/arakoodev/src/ai/src/tests/palm2/palm2.test.ts new file mode 100644 index 000000000..38d7ae81a --- /dev/null +++ b/JS/edgechains/arakoodev/src/ai/src/tests/palm2/palm2.test.ts @@ -0,0 +1,86 @@ +import axios from "axios"; +import { Palm2AI } from "../../lib/palm2/palm2"; + +jest.mock("axios"); + +describe("Palm2AI", () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test("should generate text using the default PaLM2 text model", async () => { + const mockResponse = { + candidates: [ + { + output: "Test response", + }, + ], + }; + + (axios.request as jest.Mock).mockResolvedValueOnce({ data: mockResponse }); + + const palm2 = new Palm2AI({ apiKey: "test_api_key" }); + const response = await palm2.chat({ prompt: "test prompt" }); + + expect(axios.request).toHaveBeenCalledWith( + expect.objectContaining({ + method: "post", + url: "https://generativelanguage.googleapis.com/v1beta3/models/text-bison-001:generateText?key=test_api_key", + headers: { + "Content-Type": "application/json", + }, + data: expect.objectContaining({ + prompt: { + text: "test prompt", + }, + temperature: 0.7, + candidate_count: 1, + max_output_tokens: 1024, + }), + }) + ); + expect(response).toEqual(mockResponse); + }); + + test("should allow overriding generation settings and model", async () => { + const mockResponse = { + candidates: [ + { + output: "Custom model response", + }, + ], + }; + + (axios.request as jest.Mock).mockResolvedValueOnce({ data: mockResponse }); + + const palm2 = new Palm2AI({ + apiKey: "test_api_key", + model: "text-bison-001", + apiVersion: "v1beta3", + }); + await palm2.chat({ + prompt: "test prompt", + model: "chat-bison-001", + temperature: 0.2, + candidate_count: 2, + max_output_tokens: 256, + top_k: 40, + top_p: 0.95, + max_retry: 1, + delay: 1, + }); + + expect(axios.request).toHaveBeenCalledWith( + expect.objectContaining({ + url: "https://generativelanguage.googleapis.com/v1beta3/models/chat-bison-001:generateText?key=test_api_key", + data: expect.objectContaining({ + temperature: 0.2, + candidate_count: 2, + max_output_tokens: 256, + top_k: 40, + top_p: 0.95, + }), + }) + ); + }); +}); diff --git a/JS/edgechains/examples/chat-with-palm2/jsonnet/main.jsonnet b/JS/edgechains/examples/chat-with-palm2/jsonnet/main.jsonnet new file mode 100644 index 000000000..e56080e87 --- /dev/null +++ b/JS/edgechains/examples/chat-with-palm2/jsonnet/main.jsonnet @@ -0,0 +1,15 @@ +local promptTemplate = ||| + You are a helpful assistant that can answer questions based on the given question. + Answer the following question: {question} +|||; + +local key = std.extVar('palm2_api_key'); +local UserQuestion = std.extVar('question'); + +local promptWithQuestion = std.strReplace(promptTemplate, '{question}', UserQuestion + '\n'); + +local main() = + local response = arakoo.native('palm2Call')({ prompt: promptWithQuestion, palm2ApiKey: key }); + response; + +main() diff --git a/JS/edgechains/examples/chat-with-palm2/jsonnet/secrets.jsonnet b/JS/edgechains/examples/chat-with-palm2/jsonnet/secrets.jsonnet new file mode 100644 index 000000000..695ef9421 --- /dev/null +++ b/JS/edgechains/examples/chat-with-palm2/jsonnet/secrets.jsonnet @@ -0,0 +1,3 @@ +{ + palm2_api_key: std.extVar('palm2_api_key'), +} diff --git a/JS/edgechains/examples/chat-with-palm2/package.json b/JS/edgechains/examples/chat-with-palm2/package.json new file mode 100644 index 000000000..111085930 --- /dev/null +++ b/JS/edgechains/examples/chat-with-palm2/package.json @@ -0,0 +1,18 @@ +{ + "name": "chat-with-palm2", + "version": "1.0.0", + "type": "module", + "scripts": { + "start": "tsc && node --experimental-wasm-modules ./dist/index.js" + }, + "dependencies": { + "@arakoodev/edgechains.js": "file:../../arakoodev", + "@arakoodev/jsonnet": "^0.24.0", + "file-uri-to-path": "^2.0.0", + "path": "^0.12.7", + "typescript": "^5.7.0-dev.20241007" + }, + "devDependencies": { + "@types/node": "^22.7.4" + } +} diff --git a/JS/edgechains/examples/chat-with-palm2/src/index.ts b/JS/edgechains/examples/chat-with-palm2/src/index.ts new file mode 100644 index 000000000..de5d823b7 --- /dev/null +++ b/JS/edgechains/examples/chat-with-palm2/src/index.ts @@ -0,0 +1,30 @@ +import { ArakooServer } from "@arakoodev/edgechains.js/arakooserver"; +import Jsonnet from "@arakoodev/jsonnet"; +import { createSyncRPC } from "@arakoodev/edgechains.js/sync-rpc"; +import fileURLToPath from "file-uri-to-path"; +import path from "path"; + +const server = new ArakooServer(); +const app = server.createApp(); +const jsonnet = new Jsonnet(); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const palm2Call = createSyncRPC(path.join(__dirname, "./lib/generateResponse.cjs")); + +app.post("/chat", async (c: any) => { + try { + const { question } = await c.req.json(); + const key = JSON.parse( + jsonnet.evaluateFile(path.join(__dirname, "../jsonnet/secrets.jsonnet")) + ).palm2_api_key; + jsonnet.extString("palm2_api_key", key); + jsonnet.extString("question", question || ""); + jsonnet.javascriptCallback("palm2Call", palm2Call); + const response = jsonnet.evaluateFile(path.join(__dirname, "../jsonnet/main.jsonnet")); + return c.json(JSON.parse(response)); + } catch (error) { + console.log("error occured", error); + } +}); + +server.listen(3000); diff --git a/JS/edgechains/examples/chat-with-palm2/src/lib/generateResponse.cts b/JS/edgechains/examples/chat-with-palm2/src/lib/generateResponse.cts new file mode 100644 index 000000000..71e03bf3f --- /dev/null +++ b/JS/edgechains/examples/chat-with-palm2/src/lib/generateResponse.cts @@ -0,0 +1,16 @@ +const { Palm2AI } = require("@arakoodev/edgechains.js/ai"); + +async function palm2Call({ prompt, palm2ApiKey }: any) { + try { + const palm2 = new Palm2AI({ apiKey: palm2ApiKey }); + const response = await palm2.chat({ prompt }); + return JSON.stringify({ + answer: response.candidates?.[0]?.output ?? "", + raw: response, + }); + } catch (error) { + return error; + } +} + +module.exports = palm2Call; diff --git a/JS/edgechains/examples/chat-with-palm2/tsconfig.json b/JS/edgechains/examples/chat-with-palm2/tsconfig.json new file mode 100644 index 000000000..65dd0c174 --- /dev/null +++ b/JS/edgechains/examples/chat-with-palm2/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src/**/*"] +} From 3ad0d47312a4ff1556a58883461e6b33c2bad347 Mon Sep 17 00:00:00 2001 From: shelabastida933-pixel <308737702+shelabastida933-pixel@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:48:59 +0800 Subject: [PATCH 2/2] Strengthen Palm2 request tests --- .../arakoodev/src/ai/src/lib/palm2/palm2.ts | 7 +- .../src/ai/src/tests/palm2/palm2.test.ts | 133 +++++++++++------- .../arakoodev/testcases/palm2/basic.jsonnet | 11 ++ 3 files changed, 97 insertions(+), 54 deletions(-) create mode 100644 JS/edgechains/arakoodev/testcases/palm2/basic.jsonnet diff --git a/JS/edgechains/arakoodev/src/ai/src/lib/palm2/palm2.ts b/JS/edgechains/arakoodev/src/ai/src/lib/palm2/palm2.ts index 5313ee598..f8ce341e1 100644 --- a/JS/edgechains/arakoodev/src/ai/src/lib/palm2/palm2.ts +++ b/JS/edgechains/arakoodev/src/ai/src/lib/palm2/palm2.ts @@ -9,6 +9,7 @@ export interface Palm2AIConstructionOptions { apiKey?: string; model?: string; apiVersion?: string; + baseUrl?: string; } export interface Palm2AIChatOptions { @@ -45,11 +46,13 @@ export class Palm2AI { apiKey: string; model: string; apiVersion: string; + baseUrl: string; constructor(options: Palm2AIConstructionOptions = {}) { this.apiKey = options.apiKey || process.env.PALM2_API_KEY || process.env.GOOGLE_API_KEY || ""; this.model = options.model || DEFAULT_MODEL; this.apiVersion = options.apiVersion || DEFAULT_API_VERSION; + this.baseUrl = (options.baseUrl || GOOGLE_GENERATIVE_LANGUAGE_BASE_URL).replace(/\/$/, ""); this.checkKeys(); } @@ -63,7 +66,9 @@ export class Palm2AI { async chat(chatOptions: Palm2AIChatOptions): Promise { const model = chatOptions.model || this.model; - const url = `${GOOGLE_GENERATIVE_LANGUAGE_BASE_URL}/${this.apiVersion}/models/${model}:generateText?key=${this.apiKey}`; + const url = `${this.baseUrl}/${this.apiVersion}/models/${model}:generateText?key=${encodeURIComponent( + this.apiKey + )}`; const data = { prompt: { text: chatOptions.prompt, diff --git a/JS/edgechains/arakoodev/src/ai/src/tests/palm2/palm2.test.ts b/JS/edgechains/arakoodev/src/ai/src/tests/palm2/palm2.test.ts index 38d7ae81a..9424cea84 100644 --- a/JS/edgechains/arakoodev/src/ai/src/tests/palm2/palm2.test.ts +++ b/JS/edgechains/arakoodev/src/ai/src/tests/palm2/palm2.test.ts @@ -1,62 +1,90 @@ -import axios from "axios"; +import { createServer, Server } from "http"; +import { AddressInfo } from "net"; import { Palm2AI } from "../../lib/palm2/palm2"; -jest.mock("axios"); +type CapturedRequest = { + method?: string; + url?: string; + body: any; +}; describe("Palm2AI", () => { - beforeEach(() => { - jest.resetAllMocks(); - }); + let server: Server; + let baseUrl: string; + let capturedRequest: CapturedRequest | undefined; + const mockResponse = { + candidates: [ + { + output: "Test response", + }, + ], + }; - test("should generate text using the default PaLM2 text model", async () => { - const mockResponse = { - candidates: [ - { - output: "Test response", - }, - ], - }; + beforeEach(async () => { + capturedRequest = undefined; + server = createServer((req, res) => { + let rawBody = ""; + req.on("data", (chunk) => { + rawBody += chunk; + }); + req.on("end", () => { + capturedRequest = { + method: req.method, + url: req.url, + body: JSON.parse(rawBody), + }; + res.writeHead(200, { + "Access-Control-Allow-Origin": "*", + "Content-Type": "application/json", + }); + res.end(JSON.stringify(mockResponse)); + }); + }); - (axios.request as jest.Mock).mockResolvedValueOnce({ data: mockResponse }); + await new Promise((resolve) => { + server.listen(0, "127.0.0.1", resolve); + }); + const { port } = server.address() as AddressInfo; + baseUrl = `http://127.0.0.1:${port}`; + }); - const palm2 = new Palm2AI({ apiKey: "test_api_key" }); + afterEach(async () => { + await new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); + }); + + test("should generate text using the default PaLM2 text model", async () => { + const palm2 = new Palm2AI({ apiKey: "test_api_key", baseUrl }); const response = await palm2.chat({ prompt: "test prompt" }); - expect(axios.request).toHaveBeenCalledWith( - expect.objectContaining({ - method: "post", - url: "https://generativelanguage.googleapis.com/v1beta3/models/text-bison-001:generateText?key=test_api_key", - headers: { - "Content-Type": "application/json", + expect(capturedRequest).toEqual({ + method: "POST", + url: "/v1beta3/models/text-bison-001:generateText?key=test_api_key", + body: expect.objectContaining({ + prompt: { + text: "test prompt", }, - data: expect.objectContaining({ - prompt: { - text: "test prompt", - }, - temperature: 0.7, - candidate_count: 1, - max_output_tokens: 1024, - }), - }) - ); + temperature: 0.7, + candidate_count: 1, + max_output_tokens: 1024, + }), + }); expect(response).toEqual(mockResponse); }); test("should allow overriding generation settings and model", async () => { - const mockResponse = { - candidates: [ - { - output: "Custom model response", - }, - ], - }; - - (axios.request as jest.Mock).mockResolvedValueOnce({ data: mockResponse }); - const palm2 = new Palm2AI({ apiKey: "test_api_key", model: "text-bison-001", apiVersion: "v1beta3", + baseUrl, }); await palm2.chat({ prompt: "test prompt", @@ -70,17 +98,16 @@ describe("Palm2AI", () => { delay: 1, }); - expect(axios.request).toHaveBeenCalledWith( - expect.objectContaining({ - url: "https://generativelanguage.googleapis.com/v1beta3/models/chat-bison-001:generateText?key=test_api_key", - data: expect.objectContaining({ - temperature: 0.2, - candidate_count: 2, - max_output_tokens: 256, - top_k: 40, - top_p: 0.95, - }), - }) - ); + expect(capturedRequest).toEqual({ + method: "POST", + url: "/v1beta3/models/chat-bison-001:generateText?key=test_api_key", + body: expect.objectContaining({ + temperature: 0.2, + candidate_count: 2, + max_output_tokens: 256, + top_k: 40, + top_p: 0.95, + }), + }); }); }); diff --git a/JS/edgechains/arakoodev/testcases/palm2/basic.jsonnet b/JS/edgechains/arakoodev/testcases/palm2/basic.jsonnet new file mode 100644 index 000000000..e31183f35 --- /dev/null +++ b/JS/edgechains/arakoodev/testcases/palm2/basic.jsonnet @@ -0,0 +1,11 @@ +{ + name: 'palm2-basic-generate-text', + provider: 'palm2', + model: 'text-bison-001', + input: { + prompt: 'Answer in one sentence: what is EdgeChains?', + temperature: 0.2, + candidate_count: 1, + max_output_tokens: 128, + }, +}