Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions JS/edgechains/arakoodev/src/ai/src/index.ts
Original file line number Diff line number Diff line change
@@ -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";
192 changes: 192 additions & 0 deletions JS/edgechains/arakoodev/src/ai/src/lib/palm2/palm2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import axios from "axios";
import { retry } from "@lifeomic/attempt";

const baseUrl = "https://generativelanguage.googleapis.com/v1beta2/models";

type Palm2TextModel = "text-bison-001";
type Palm2ChatModel = "chat-bison-001";
type Palm2EmbeddingModel = "embedding-gecko-001";

interface Palm2ConstructionOptions {
apiKey?: string;
}

type SafetyRating = {
category: string;
probability: "NEGLIGIBLE" | "LOW" | "MEDIUM" | "HIGH";
};

type ContentFilter = {
reason: string;
message?: string;
};

type TextCompletion = {
output: string;
safetyRatings?: SafetyRating[];
};

interface Palm2GenerateTextResponse {
candidates: TextCompletion[];
filters?: ContentFilter[];
safetyFeedback?: object[];
}

type Message = {
author?: string;
content: string;
};

type Example = {
input: Message;
output: Message;
};

interface Palm2GenerateMessageResponse {
candidates: Message[];
messages: Message[];
filters?: ContentFilter[];
}

interface Palm2EmbeddingResponse {
embedding: {
value: number[];
};
}

interface Palm2GenerateTextOptions {
model?: Palm2TextModel;
prompt: string;
temperature?: number;
max_output_tokens?: number;
candidate_count?: number;
top_p?: number;
top_k?: number;
stop_sequences?: string[];
max_retry?: number;
delay?: number;
}

interface Palm2ChatOptions {
model?: Palm2ChatModel;
prompt?: string;
context?: string;
examples?: Example[];
messages?: Message[];
temperature?: number;
candidate_count?: number;
top_p?: number;
top_k?: number;
max_retry?: number;
delay?: number;
}

interface Palm2EmbeddingOptions {
model?: Palm2EmbeddingModel;
text: string;
max_retry?: number;
delay?: number;
}

// Strip undefined keys so optional params never leak onto the wire payload.
function removeUndefined<T extends Record<string, any>>(obj: T): Partial<T> {
return Object.fromEntries(
Object.entries(obj).filter(([, value]) => value !== undefined)
) as Partial<T>;
}

export class Palm2AI {
apiKey: string;
constructor(options: Palm2ConstructionOptions = {}) {
this.apiKey = options.apiKey || process.env.PALM_API_KEY || "";
}

private async request<T>(
path: string,
body: object,
max_retry?: number,
delay?: number
): Promise<T> {
if (!this.apiKey) {
throw new Error(
"API key is missing. Please provide a valid Palm2 API key, or set it in the PALM_API_KEY environment variable."
);
}
const config = {
method: "post",
maxBodyLength: Infinity,
url: `${baseUrl}/${path}`,
headers: {
"Content-Type": "application/json",
"x-goog-api-key": this.apiKey,
},
data: JSON.stringify(body),
};
return await retry(
async () => {
return (await axios.request(config)).data;
},
{ maxAttempts: max_retry || 3, delay: delay || 200 }
);
}

async generateText(options: Palm2GenerateTextOptions): Promise<Palm2GenerateTextResponse> {
if (!options.prompt) {
throw new Error("prompt is required to generate text with Palm2.");
}
const body = removeUndefined({
prompt: { text: options.prompt },
temperature: options.temperature ?? 0.7,
candidateCount: options.candidate_count ?? 1,
maxOutputTokens: options.max_output_tokens ?? 1024,
topP: options.top_p,
topK: options.top_k,
stopSequences: options.stop_sequences,
});
return this.request<Palm2GenerateTextResponse>(
`${options.model || "text-bison-001"}:generateText`,
body,
options.max_retry,
options.delay
);
}

async chat(options: Palm2ChatOptions): Promise<Palm2GenerateMessageResponse> {
if (!options.prompt && (!options.messages || options.messages.length === 0)) {
throw new Error(
"Either 'prompt' or a non-empty 'messages' array is required to chat with Palm2."
);
}
const messages = options.prompt ? [{ content: options.prompt }] : options.messages;
const body = removeUndefined({
prompt: removeUndefined({
context: options.context,
examples: options.examples,
messages,
}),
temperature: options.temperature ?? 0.7,
candidateCount: options.candidate_count ?? 1,
topP: options.top_p,
topK: options.top_k,
});
return this.request<Palm2GenerateMessageResponse>(
`${options.model || "chat-bison-001"}:generateMessage`,
body,
options.max_retry,
options.delay
);
}

async generateEmbeddings(options: Palm2EmbeddingOptions): Promise<Palm2EmbeddingResponse> {
if (!options.text) {
throw new Error("text is required to generate embeddings with Palm2.");
}
const body = { text: options.text };
return this.request<Palm2EmbeddingResponse>(
`${options.model || "embedding-gecko-001"}:embedText`,
body,
options.max_retry,
options.delay
);
}
}
96 changes: 96 additions & 0 deletions JS/edgechains/arakoodev/src/ai/src/tests/palm2/palm2.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import axios from "axios";
import { Palm2AI } from "../../lib/palm2/palm2";

jest.mock("axios");
const mockedAxios = axios as jest.Mocked<typeof axios>;

describe("Palm2AI", () => {
afterEach(() => {
jest.clearAllMocks();
});

describe("generateText", () => {
test("should generate text completion from Palm2", async () => {
const mockResponse = {
candidates: [{ output: "Test response", safetyRatings: [] }],
};
mockedAxios.request.mockResolvedValueOnce({ data: mockResponse } as any);
const palm2 = new Palm2AI({ apiKey: "test_api_key" });
const response = await palm2.generateText({ prompt: "test prompt" });
expect(response.candidates[0].output).toEqual("Test response");
});

test("should send prompt, model and generation config in the request body", async () => {
mockedAxios.request.mockResolvedValueOnce({ data: { candidates: [] } } as any);
const palm2 = new Palm2AI({ apiKey: "test_api_key" });
await palm2.generateText({ prompt: "hello", temperature: 0, max_output_tokens: 16 });
const config = mockedAxios.request.mock.calls[0][0] as any;
const body = JSON.parse(config.data);
expect(config.url).toContain("text-bison-001:generateText");
expect(config.headers["x-goog-api-key"]).toEqual("test_api_key");
expect(body.prompt.text).toEqual("hello");
// temperature: 0 is a valid value and must NOT fall back to the 0.7 default.
expect(body.temperature).toEqual(0);
expect(body.maxOutputTokens).toEqual(16);
// Untouched optional knobs must be stripped from the payload.
expect(body).not.toHaveProperty("topP");
expect(body).not.toHaveProperty("topK");
});

test("should reject when prompt is empty", async () => {
const palm2 = new Palm2AI({ apiKey: "test_api_key" });
await expect(palm2.generateText({ prompt: "" })).rejects.toThrow("prompt is required");
expect(mockedAxios.request).not.toHaveBeenCalled();
});
});

describe("chat", () => {
test("should generate a chat message from Palm2", async () => {
const mockResponse = {
candidates: [{ author: "1", content: "Hello there" }],
messages: [{ author: "0", content: "hi" }],
};
mockedAxios.request.mockResolvedValueOnce({ data: mockResponse } as any);
const palm2 = new Palm2AI({ apiKey: "test_api_key" });
const response = await palm2.chat({ prompt: "hi" });
expect(response.candidates[0].content).toEqual("Hello there");
});

test("should wrap prompt as a single message and target generateMessage", async () => {
mockedAxios.request.mockResolvedValueOnce({
data: { candidates: [], messages: [] },
} as any);
const palm2 = new Palm2AI({ apiKey: "test_api_key" });
await palm2.chat({ prompt: "hi", context: "be terse" });
const config = mockedAxios.request.mock.calls[0][0] as any;
const body = JSON.parse(config.data);
expect(config.url).toContain("chat-bison-001:generateMessage");
expect(body.prompt.context).toEqual("be terse");
expect(body.prompt.messages).toEqual([{ content: "hi" }]);
});

test("should reject when neither prompt nor messages are provided", async () => {
const palm2 = new Palm2AI({ apiKey: "test_api_key" });
await expect(palm2.chat({})).rejects.toThrow();
expect(mockedAxios.request).not.toHaveBeenCalled();
});
});

describe("generateEmbeddings", () => {
test("should generate embeddings from Palm2", async () => {
const mockResponse = { embedding: { value: [0.1, 0.2, 0.3] } };
mockedAxios.request.mockResolvedValueOnce({ data: mockResponse } as any);
const palm2 = new Palm2AI({ apiKey: "test_api_key" });
const response = await palm2.generateEmbeddings({ text: "embed me" });
expect(response.embedding.value).toEqual([0.1, 0.2, 0.3]);
});
});

describe("authentication", () => {
test("should reject when no api key is available", async () => {
const palm2 = new Palm2AI({ apiKey: "" });
await expect(palm2.generateText({ prompt: "hi" })).rejects.toThrow("API key is missing");
expect(mockedAxios.request).not.toHaveBeenCalled();
});
});
});
2 changes: 2 additions & 0 deletions JS/edgechains/examples/palm2/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
dist
16 changes: 16 additions & 0 deletions JS/edgechains/examples/palm2/jsonnet/main.jsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
local promptTemplate = |||
You are a helpful assistant that can answer questions based on 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()
6 changes: 6 additions & 0 deletions JS/edgechains/examples/palm2/jsonnet/secrets.jsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

local PALM2_API_KEY = "***";

{
"palm2_api_key":PALM2_API_KEY,
}
23 changes: 23 additions & 0 deletions JS/edgechains/examples/palm2/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "palm2-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"keywords": [],
"author": "",
"scripts": {
"start": "tsc && node --experimental-wasm-modules ./dist/index.js"
},
"license": "ISC",
"dependencies": {
"@arakoodev/edgechains.js": "file:../../arakoodev",
"@arakoodev/jsonnet": "^0.24.0",
"file-uri-to-path": "^2.0.0",
"path": "^0.12.7"
},
"devDependencies": {
"@types/node": "^22.7.4",
"typescript": "^5.7.0-dev.20241007"
}
}
39 changes: 39 additions & 0 deletions JS/edgechains/examples/palm2/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Palm2 Example

This example shows how to call Google's **Palm2** (`text-bison-001`) API through the
`Palm2AI` class from `@arakoodev/edgechains.js/ai`. The prompt lives in a jsonnet file
(`jsonnet/main.jsonnet`) and is **not** hardcoded in the TypeScript source.

## Installation

1. Install the required dependencies:

```bash
npm install
```

## Configuration

1. Add your Palm2 API key in `jsonnet/secrets.jsonnet`:

```jsonnet
local PALM2_API_KEY = "***";
```

You can also set it through the `PALM_API_KEY` environment variable.

## Usage

1. Start the server:

```bash
npm run start
```

2. Hit the `POST` endpoint with a basic question at `http://localhost:3000/chat`.

```bash
body = {
"question": "hi"
}
```
Loading
Loading