From a8a425d334a7c9b5f9ec5be8cc7aae547c2f0a3f Mon Sep 17 00:00:00 2001 From: ngoc Date: Sun, 18 Jan 2026 13:26:38 +0700 Subject: [PATCH] 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}}