Merge pull request #3 from arkhai-io/refactor
Feat: Support websearch and update ts-sdk to newest version
This commit is contained in:
commit
ef3d3b1dd0
|
|
@ -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-...
|
||||
|
|
|
|||
10
bun.lock
10
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=="],
|
||||
|
|
@ -67,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=="],
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
@ -23,6 +24,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 +36,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 +64,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 +121,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 +157,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}`);
|
||||
}
|
||||
|
|
@ -178,24 +187,27 @@ async function main() {
|
|||
// Add all available providers
|
||||
if (openaiApiKey) {
|
||||
llmClient.llm.addProvider({
|
||||
providerName: "OpenAI",
|
||||
providerName: ProviderName.OpenAI,
|
||||
apiKey: openaiApiKey,
|
||||
perplexityApiKey: perplexityApiKey,
|
||||
});
|
||||
console.log("✅ OpenAI provider configured");
|
||||
}
|
||||
|
||||
if (anthropicApiKey) {
|
||||
llmClient.llm.addProvider({
|
||||
providerName: "Anthropic",
|
||||
providerName: ProviderName.Anthropic,
|
||||
apiKey: anthropicApiKey,
|
||||
perplexityApiKey: perplexityApiKey,
|
||||
});
|
||||
console.log("✅ Anthropic provider configured");
|
||||
}
|
||||
|
||||
if (openrouterApiKey) {
|
||||
llmClient.llm.addProvider({
|
||||
providerName: "OpenRouter",
|
||||
providerName: ProviderName.OpenRouter,
|
||||
apiKey: openrouterApiKey,
|
||||
perplexityApiKey: perplexityApiKey,
|
||||
});
|
||||
console.log("✅ OpenRouter provider configured");
|
||||
}
|
||||
|
|
@ -207,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}`);
|
||||
|
|
|
|||
53
nla.ts
53
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 <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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
|
|
@ -67,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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue