From a8a425d334a7c9b5f9ec5be8cc7aae547c2f0a3f Mon Sep 17 00:00:00 2001 From: ngoc Date: Sun, 18 Jan 2026 13:26:38 +0700 Subject: [PATCH 1/3] Refactor nla cli and add websearch --- .env.example | 5 ++++ bun.lock | 8 +++++++ cli/scripts/dev.sh | 31 +++++++++++++++++------- cli/server/oracle.ts | 11 +++++++++ nla.ts | 53 ++++++++++++++++++++++++++++++++++------- package.json | 2 ++ tests/nlaOracle.test.ts | 15 +++++++++--- 7 files changed, 104 insertions(+), 21 deletions(-) diff --git a/.env.example b/.env.example index aae978b..cefda76 100644 --- a/.env.example +++ b/.env.example @@ -50,3 +50,8 @@ OPENAI_API_KEY=sk-proj... # Get your API key from https://openrouter.ai/keys # Supports any model available on OpenRouter (e.g., openai/gpt-4, anthropic/claude-3-opus) # OPENROUTER_API_KEY=sk-or-... + +# Perplexity API Key (optional) +# Get your API key from https://www.perplexity.ai/settings/api +# Used for enhanced search capabilities with LLM providers +# PERPLEXITY_API_KEY=pplx-... diff --git a/bun.lock b/bun.lock index 39b4761..bb5623c 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,8 @@ "dependencies": { "@ai-sdk/anthropic": "^3.0.2", "@ai-sdk/openai": "^3.0.2", + "@openrouter/ai-sdk-provider": "^1.5.4", + "@perplexity-ai/ai-sdk": "^0.1.2", "@viem/anvil": "^0.0.10", "ai": "^6.0.5", "alkahest-ts": "github:arkhai-io/alkahest", @@ -45,8 +47,14 @@ "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@1.5.4", "", { "dependencies": { "@openrouter/sdk": "^0.1.27" }, "peerDependencies": { "ai": "^5.0.0", "zod": "^3.24.1 || ^v4" } }, "sha512-xrSQPUIH8n9zuyYZR0XK7Ba0h2KsjJcMkxnwaYfmv13pKs3sDkjPzVPPhlhzqBGddHb5cFEwJ9VFuFeDcxCDSw=="], + + "@openrouter/sdk": ["@openrouter/sdk@0.1.27", "", { "dependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-RH//L10bSmc81q25zAZudiI4kNkLgxF2E+WU42vghp3N6TEvZ6F0jK7uT3tOxkEn91gzmMw9YVmDENy7SJsajQ=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + "@perplexity-ai/ai-sdk": ["@perplexity-ai/ai-sdk@0.1.2", "", { "peerDependencies": { "ai": "^5.0.0", "zod": "^4.0.0" } }, "sha512-7/f6zFA0ND48wMPlJzBqpm+LH4g3GdsVBVAb2LZn9Zt4V6rg/uzy9XTduTovbZa9YdaYTLg4wS6UpxH21ssU3g=="], + "@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], "@scure/bip32": ["@scure/bip32@1.7.0", "", { "dependencies": { "@noble/curves": "~1.9.0", "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw=="], diff --git a/cli/scripts/dev.sh b/cli/scripts/dev.sh index 2bbe694..cfa0cea 100755 --- a/cli/scripts/dev.sh +++ b/cli/scripts/dev.sh @@ -63,14 +63,29 @@ if [ ! -d "../alkahest" ]; then fi echo -e "${GREEN}✅ alkahest repository found${NC}" -# Check OpenAI API key -if [ -z "$OPENAI_API_KEY" ]; then - echo -e "${RED}❌ OPENAI_API_KEY not set${NC}" - echo "Please create a .env file with: OPENAI_API_KEY=sk-your-key-here" - echo "Or export it: export OPENAI_API_KEY=sk-your-key-here" +# Check LLM API keys +if [ -z "$OPENAI_API_KEY" ] && [ -z "$ANTHROPIC_API_KEY" ] && [ -z "$OPENROUTER_API_KEY" ]; then + echo -e "${RED}❌ No LLM provider API key set${NC}" + echo "Please add at least one API key to your .env file:" + echo " OPENAI_API_KEY=sk-..." + echo " ANTHROPIC_API_KEY=sk-ant-..." + echo " OPENROUTER_API_KEY=sk-or-..." exit 1 fi -echo -e "${GREEN}✅ OpenAI API key configured${NC}\n" + +if [ -n "$OPENAI_API_KEY" ]; then + echo -e "${GREEN}✅ OpenAI API key configured${NC}" +fi +if [ -n "$ANTHROPIC_API_KEY" ]; then + echo -e "${GREEN}✅ Anthropic API key configured${NC}" +fi +if [ -n "$OPENROUTER_API_KEY" ]; then + echo -e "${GREEN}✅ OpenRouter API key configured${NC}" +fi +if [ -n "$PERPLEXITY_API_KEY" ]; then + echo -e "${GREEN}✅ Perplexity API key configured${NC}" +fi +echo "" # Install dependencies echo -e "${BLUE}📦 Installing dependencies...${NC}\n" @@ -102,13 +117,11 @@ fi # Deploy contracts echo -e "\n${BLUE}📝 Deploying contracts...${NC}\n" -export DEPLOYER_PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" bun run cli/server/deploy.ts --network localhost --rpc-url http://localhost:8545 # Start oracle echo -e "\n${BLUE}🚀 Starting oracle...${NC}\n" -export ORACLE_PRIVATE_KEY="0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" -bun run cli/server/oracle.ts --deployment ./cli/deployments/localhost.json --openai-api-key "$OPENAI_API_KEY" --private-key "$ORACLE_PRIVATE_KEY" +bun run cli/server/oracle.ts --deployment ./cli/deployments/localhost.json # Cleanup function cleanup() { diff --git a/cli/server/oracle.ts b/cli/server/oracle.ts index fdc9754..43f4eef 100644 --- a/cli/server/oracle.ts +++ b/cli/server/oracle.ts @@ -23,6 +23,7 @@ Options: --openai-api-key OpenAI API key (optional) --anthropic-api-key Anthropic API key (optional) --openrouter-api-key OpenRouter API key (optional) + --perplexity-api-key Perplexity API key for search tools (optional) --eas-contract
EAS contract address (optional) --deployment Load addresses from deployment file (optional) --polling-interval Polling interval in milliseconds (default: 5000) @@ -34,6 +35,7 @@ Environment Variables (alternative to CLI options): OPENAI_API_KEY OpenAI API key ANTHROPIC_API_KEY Anthropic API key OPENROUTER_API_KEY OpenRouter API key + PERPLEXITY_API_KEY Perplexity API key for search tools EAS_CONTRACT_ADDRESS EAS contract address Examples: @@ -61,6 +63,7 @@ function parseCliArgs() { "openai-api-key": { type: "string" }, "anthropic-api-key": { type: "string" }, "openrouter-api-key": { type: "string" }, + "perplexity-api-key": { type: "string" }, "eas-contract": { type: "string" }, "deployment": { type: "string" }, "polling-interval": { type: "string" }, @@ -117,6 +120,7 @@ async function main() { const openaiApiKey = args["openai-api-key"] || process.env.OPENAI_API_KEY; const anthropicApiKey = args["anthropic-api-key"] || process.env.ANTHROPIC_API_KEY; const openrouterApiKey = args["openrouter-api-key"] || process.env.OPENROUTER_API_KEY; + const perplexityApiKey = args["perplexity-api-key"] || process.env.PERPLEXITY_API_KEY; const pollingInterval = parseInt(args["polling-interval"] || "5000"); // Validate required parameters @@ -152,6 +156,10 @@ async function main() { if (openrouterApiKey) availableProviders.push("OpenRouter"); console.log(` 🤖 AI Providers: ${availableProviders.join(", ")}`); + if (perplexityApiKey) { + console.log(` 🔍 Perplexity Search: Enabled`); + } + if (easContract) { console.log(` 📝 EAS Contract: ${easContract}`); } @@ -180,6 +188,7 @@ async function main() { llmClient.llm.addProvider({ providerName: "OpenAI", apiKey: openaiApiKey, + perplexityApiKey: perplexityApiKey, }); console.log("✅ OpenAI provider configured"); } @@ -188,6 +197,7 @@ async function main() { llmClient.llm.addProvider({ providerName: "Anthropic", apiKey: anthropicApiKey, + perplexityApiKey: perplexityApiKey, }); console.log("✅ Anthropic provider configured"); } @@ -196,6 +206,7 @@ async function main() { llmClient.llm.addProvider({ providerName: "OpenRouter", apiKey: openrouterApiKey, + perplexityApiKey: perplexityApiKey, }); console.log("✅ OpenRouter provider configured"); } diff --git a/nla.ts b/nla.ts index 50840cf..fd6edac 100644 --- a/nla.ts +++ b/nla.ts @@ -49,15 +49,23 @@ import { encodeAbiParameters, parseAbiParameters, } from "viem"; -import { generateText } from "ai"; +import { generateText, stepCountIs } from "ai"; import { createOpenAI } from "@ai-sdk/openai"; import { createAnthropic } from "@ai-sdk/anthropic"; +import { createOpenRouter } from '@openrouter/ai-sdk-provider'; +import { perplexitySearch } from '@perplexity-ai/ai-sdk'; +export enum ProviderName { + OpenAI = "OpenAI", + Anthropic = "Anthropic", + OpenRouter = "OpenRouter", +} export type LLMProvider = { - providerName: string; + providerName: ProviderName | string; apiKey?: string; baseURL?: string; // For OpenRouter or custom endpoints + perplexityApiKey?: string; // For Perplexity AI }; export type LLMDemand = { @@ -67,6 +75,22 @@ export type LLMDemand = { demand: string; }; +const cleanBool = (raw: string) => { + let t = raw.trim().toLowerCase(); + + // strip ... + t = t.replace(/<\/?[^>]+(>|$)/g, "").trim(); + + // strip code fences + t = t.replace(/```[\s\S]*?```/g, "").trim(); + + // strip result: and similar prefixes + t = t.replace(/^(result:|answer:)\s*/g, "").trim(); + + return t; +}; + + export const makeLLMClient = ( providers: LLMProvider[], ) => { @@ -88,6 +112,7 @@ export const makeLLMClient = ( const arbitrate = async (demand: LLMDemand, obligation: string): Promise => { try { + console.log(demand); const matchingProvider = providers.find(provider => provider.providerName.toLowerCase().includes(demand.arbitrationProvider.toLowerCase()) || demand.arbitrationProvider.toLowerCase().includes(provider.providerName.toLowerCase()) @@ -113,8 +138,7 @@ Answer ONLY with 'true' or 'false' - no explanations or additional text.`; let text: string; const providerName = selectedProvider.providerName.toLowerCase(); - - if (providerName === 'openai' || providerName.includes('openai')) { + if (providerName === ProviderName.OpenAI.toLowerCase() || providerName.includes('openai')) { const openai = createOpenAI({ apiKey: selectedProvider.apiKey, baseURL: selectedProvider.baseURL, @@ -122,12 +146,15 @@ Answer ONLY with 'true' or 'false' - no explanations or additional text.`; const result = await generateText({ model: openai(demand.arbitrationModel), + tools: { + web_search: openai.tools.webSearch({}), + }, system: systemPrompt, prompt: userPrompt, }); text = result.text; - } else if (providerName === 'anthropic' || providerName.includes('anthropic') || providerName.includes('claude')) { + } else if (providerName === ProviderName.Anthropic.toLowerCase() || providerName.includes('anthropic') || providerName.includes('claude')) { const anthropic = createAnthropic({ apiKey: selectedProvider.apiKey, baseURL: selectedProvider.baseURL, @@ -135,22 +162,30 @@ Answer ONLY with 'true' or 'false' - no explanations or additional text.`; const result = await generateText({ model: anthropic(demand.arbitrationModel), + tools: { + search: perplexitySearch({apiKey: selectedProvider.perplexityApiKey}), + }, system: systemPrompt, prompt: userPrompt, + }); text = result.text; - } else if (providerName === 'openrouter' || providerName.includes('openrouter')) { + } else if (providerName === ProviderName.OpenRouter.toLowerCase() || providerName.includes('openrouter')) { // OpenRouter uses OpenAI-compatible API - const openrouter = createOpenAI({ + const openrouter = createOpenRouter({ apiKey: selectedProvider.apiKey, baseURL: selectedProvider.baseURL, }); const result = await generateText({ - model: openrouter(demand.arbitrationModel), + model: openrouter.chat(demand.arbitrationModel), + tools: { + search: perplexitySearch({apiKey: selectedProvider.perplexityApiKey}), + }, system: systemPrompt, prompt: userPrompt, + maxOutputTokens: 512, }); text = result.text; @@ -160,7 +195,7 @@ Answer ONLY with 'true' or 'false' - no explanations or additional text.`; console.log(`LLM Response: ${text}`); - const cleanedResponse = text.trim().toLowerCase(); + const cleanedResponse = cleanBool(text); return cleanedResponse === 'true'; } catch (error) { diff --git a/package.json b/package.json index fd33227..4134f9a 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "dependencies": { "@ai-sdk/anthropic": "^3.0.2", "@ai-sdk/openai": "^3.0.2", + "@openrouter/ai-sdk-provider": "^1.5.4", + "@perplexity-ai/ai-sdk": "^0.1.2", "@viem/anvil": "^0.0.10", "ai": "^6.0.5", "alkahest-ts": "github:arkhai-io/alkahest", diff --git a/tests/nlaOracle.test.ts b/tests/nlaOracle.test.ts index f8154ae..8268cf9 100644 --- a/tests/nlaOracle.test.ts +++ b/tests/nlaOracle.test.ts @@ -10,6 +10,7 @@ import { } from "alkahest-ts"; import { makeLLMClient } from ".."; import { de } from "zod/v4/locales"; +import { ProviderName } from "../nla"; let testContext: TestContext; let charlieClient: ReturnType }>>; @@ -20,9 +21,17 @@ beforeAll(async () => { llm: makeLLMClient([]), })); charlieClient.llm.addProvider({ - providerName: "OpenAI", + providerName: ProviderName.OpenAI, apiKey: process.env.OPENAI_API_KEY, }); + charlieClient.llm.addProvider({ + providerName: ProviderName.OpenRouter, + apiKey: process.env.OPENROUTER_API_KEY, + }); + charlieClient.llm.addProvider({ + providerName: ProviderName.Anthropic, + apiKey: process.env.ANTHROPIC_API_KEY, + }); }); beforeEach(async () => { @@ -44,8 +53,8 @@ test("listenAndArbitrate Natural Language", async () => { const demand = testContext.alice.client.arbiters.general.trustedOracle.encodeDemand({ oracle: testContext.bob.address, data: charlieClient.llm.encodeDemand({ - arbitrationProvider: "OpenAI", - arbitrationModel: "gpt-4.1", + arbitrationProvider: ProviderName.OpenRouter, + arbitrationModel: "openai/gpt-4o", arbitrationPrompt: `Evaluate the fulfillment against the demand and decide whether the demand was validly fulfilled Demand: {{demand}} From 0bb2e5b3c6f28a02960a49b60e603eaff15c402b Mon Sep 17 00:00:00 2001 From: ngoc Date: Sun, 18 Jan 2026 13:27:55 +0700 Subject: [PATCH 2/3] Use provider enum --- cli/server/oracle.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cli/server/oracle.ts b/cli/server/oracle.ts index 43f4eef..36348a8 100644 --- a/cli/server/oracle.ts +++ b/cli/server/oracle.ts @@ -8,6 +8,7 @@ import { existsSync, readFileSync } from "fs"; import { resolve } from "path"; import { makeClient } from "alkahest-ts"; import { fixtures } from "alkahest-ts"; +import { ProviderName } from "../../nla"; // Helper function to display usage function displayHelp() { @@ -186,7 +187,7 @@ async function main() { // Add all available providers if (openaiApiKey) { llmClient.llm.addProvider({ - providerName: "OpenAI", + providerName: ProviderName.OpenAI, apiKey: openaiApiKey, perplexityApiKey: perplexityApiKey, }); @@ -195,7 +196,7 @@ async function main() { if (anthropicApiKey) { llmClient.llm.addProvider({ - providerName: "Anthropic", + providerName: ProviderName.Anthropic, apiKey: anthropicApiKey, perplexityApiKey: perplexityApiKey, }); @@ -204,7 +205,7 @@ async function main() { if (openrouterApiKey) { llmClient.llm.addProvider({ - providerName: "OpenRouter", + providerName: ProviderName.OpenRouter, apiKey: openrouterApiKey, perplexityApiKey: perplexityApiKey, }); From b62a25c5ebc1af2e602ce8bbb558cf54f287c394 Mon Sep 17 00:00:00 2001 From: ngoc Date: Sun, 18 Jan 2026 13:46:52 +0700 Subject: [PATCH 3/3] Update ts package to newest version --- bun.lock | 2 +- cli/server/oracle.ts | 2 +- tests/nlaOracle.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index bb5623c..852a509 100644 --- a/bun.lock +++ b/bun.lock @@ -75,7 +75,7 @@ "ai": ["ai@6.0.5", "", { "dependencies": { "@ai-sdk/gateway": "3.0.4", "@ai-sdk/provider": "3.0.1", "@ai-sdk/provider-utils": "4.0.2", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CKL3dDHedWskC6EY67LrULonZBU9vL+Bwa+xQEcprBhJfxpogntG3utjiAkYuy5ZQatyWk+SmWG8HLvcnhvbRg=="], - "alkahest-ts": ["alkahest-ts@github:arkhai-io/alkahest#3c53bc9", {}, "arkhai-io-alkahest-3c53bc9"], + "alkahest-ts": ["alkahest-ts@github:arkhai-io/alkahest#80a8273", { "dependencies": { "@viem/anvil": "^0.0.10", "arktype": "^2.1.23", "zod": "^3.25.76" }, "peerDependencies": { "typescript": "^5.9.3", "viem": "^2.38.3" } }, "arkhai-io-alkahest-80a8273"], "arkregex": ["arkregex@0.0.5", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw=="], diff --git a/cli/server/oracle.ts b/cli/server/oracle.ts index 36348a8..57926b0 100644 --- a/cli/server/oracle.ts +++ b/cli/server/oracle.ts @@ -219,7 +219,7 @@ async function main() { const obligationAbi = parseAbiParameters("(string item)"); // Start listening and arbitrating - const { unwatch } = await client.arbiters.general.trustedOracle.listenAndArbitrate( + const { unwatch } = await client.arbiters.general.trustedOracle.arbitrateMany( async ({ attestation, demand }) => { console.log(`\n📨 New arbitration request received!`); console.log(` Attestation UID: ${attestation.uid}`); diff --git a/tests/nlaOracle.test.ts b/tests/nlaOracle.test.ts index 8268cf9..dff9241 100644 --- a/tests/nlaOracle.test.ts +++ b/tests/nlaOracle.test.ts @@ -76,7 +76,7 @@ Fulfillment: {{obligation}}`, const obligationAbi = parseAbiParameters("(string item)"); const { decisions, unwatch } = - await testContext.bob.client.arbiters.general.trustedOracle.listenAndArbitrate( + await testContext.bob.client.arbiters.general.trustedOracle.arbitrateMany( async ({ attestation, demand }) => { console.log("Arbitrating ", attestation, demand); const obligation = charlieClient.extractObligationData(