Refactor nla cli and add websearch

This commit is contained in:
ngoc 2026-01-18 13:26:38 +07:00
parent 7b50984ee0
commit a8a425d334
No known key found for this signature in database
GPG Key ID: 51FE6110113A5C32
7 changed files with 104 additions and 21 deletions

View File

@ -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-...

View File

@ -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=="],

View File

@ -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() {

View File

@ -23,6 +23,7 @@ Options:
--openai-api-key <key> OpenAI API key (optional)
--anthropic-api-key <key> Anthropic API key (optional)
--openrouter-api-key <key> OpenRouter API key (optional)
--perplexity-api-key <key> Perplexity API key for search tools (optional)
--eas-contract <address> EAS contract address (optional)
--deployment <file> Load addresses from deployment file (optional)
--polling-interval <ms> 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");
}

53
nla.ts
View File

@ -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 <tag>... </tag>
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<boolean> => {
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) {

View File

@ -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",

View File

@ -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<typeof testContext.charlie.client.extend<{ llm: ReturnType<typeof makeLLMClient> }>>;
@ -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}}