Skip to content

Commit

Permalink
Merge pull request #2 from storyprotocol/allen/add-story-apis
Browse files Browse the repository at this point in the history
Add Story API scaffold - getAllLicenses
  • Loading branch information
allenchuang authored Dec 12, 2024
2 parents c176e1e + e814a0d commit 550b110
Show file tree
Hide file tree
Showing 10 changed files with 1,374 additions and 329 deletions.
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -217,4 +217,6 @@ APTOS_NETWORK= # must be one of mainnet, testnet

# Story
STORY_PRIVATE_KEY= # Story private key
PINATA_JWT= # Pinata JWT for uploading files to IPFS
STORY_API_BASE_URL= # Story API base URL
STORY_API_KEY= # Story API key
PINATA_JWT= # Pinata JWT for uploading files to IPFS
3 changes: 3 additions & 0 deletions packages/plugin-story/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,8 @@
},
"peerDependencies": {
"whatwg-url": "7.1.0"
},
"devDependencies": {
"@types/node": "^22.10.1"
}
}
170 changes: 170 additions & 0 deletions packages/plugin-story/src/actions/getAvailableLicenses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import {
composeContext,
elizaLogger,
generateObjectDEPRECATED,
HandlerCallback,
ModelClass,
type IAgentRuntime,
type Memory,
type State,
} from "@ai16z/eliza";
import { getAvailableLicensesTemplate, licenseIPTemplate } from "../templates";
import { Address } from "viem";
import { IPLicenseDetails, RESOURCE_TYPE } from "../types/api";
import { API_KEY, API_URL } from "../lib/api";
import { storyOdyssey } from "viem/chains";

export { licenseIPTemplate };

type GetAvailableLicensesParams = {
ipid: Address;
};

type GetAvailableLicensesResponse = {
data: IPLicenseDetails[];
};

export class GetAvailableLicensesAction {
constructor() {}

async getAvailableLicenses(
params: GetAvailableLicensesParams
): Promise<GetAvailableLicensesResponse> {
const ipLicenseTermsQueryOptions = {
pagination: {
limit: 10,
offset: 0,
},
orderBy: "blockNumber",
orderDirection: "desc",
};

elizaLogger.log(
"Fetching from",
`${API_URL}/${RESOURCE_TYPE.IP_LICENSE_DETAILS}`
);
const response = await fetch(
`${API_URL}/${RESOURCE_TYPE.IP_LICENSE_DETAILS}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": API_KEY,
"x-chain": storyOdyssey.id.toString(),
},
cache: "no-cache",
body: JSON.stringify({
ip_ids: [params.ipid], // Use the provided IPID instead of hardcoded value
options: ipLicenseTermsQueryOptions, // Use the defined query options
}),
}
);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const text = await response.text();
try {
const licenseDetailsResponse = JSON.parse(text);
elizaLogger.log("licenseDetailsResponse", licenseDetailsResponse);
return licenseDetailsResponse;
} catch (e) {
elizaLogger.error("Failed to parse response:", text);
throw new Error(`Failed to parse JSON response: ${e.message}`);
}
}
}

export const getAvailableLicensesAction = {
name: "GET_AVAILABLE_LICENSES",
description: "Get available licenses for an IP Asset on Story",
handler: async (
runtime: IAgentRuntime,
message: Memory,
state: State,
options: any,
callback?: HandlerCallback
): Promise<boolean> => {
elizaLogger.log("Starting GET_AVAILABLE_LICENSES handler...");

// initialize or update state
if (!state) {
state = (await runtime.composeState(message)) as State;
} else {
state = await runtime.updateRecentMessageState(state);
}

const getAvailableLicensesContext = composeContext({
state,
template: getAvailableLicensesTemplate,
});

const content = await generateObjectDEPRECATED({
runtime,
context: getAvailableLicensesContext,
modelClass: ModelClass.SMALL,
});

const action = new GetAvailableLicensesAction();
try {
const response = await action.getAvailableLicenses(content);

// TODO: need to format this better into human understandable terms
const formattedResponse = response.data
.map((license) => {
const terms = license.terms;
return `License ID: ${license.id}
- Terms:
• Commercial Use: ${terms.commercialUse ? "Allowed" : "Not Allowed"}
• Commercial Attribution: ${terms.commercialAttribution ? "Required" : "Not Required"}
• Derivatives: ${terms.derivativesAllowed ? "Allowed" : "Not Allowed"}
• Derivatives Attribution: ${terms.derivativesAttribution ? "Required" : "Not Required"}
• Derivatives Approval: ${terms.derivativesApproval ? "Required" : "Not Required"}
• Revenue Share: ${terms.commercialRevenueShare ? terms.commercialRevenueShare + "%" : "Not Required"}
`;
})
.join("\n");

callback?.({
text: formattedResponse,
action: "GET_AVAILABLE_LICENSES",
source: "Story Protocol API",
});
return true;
} catch (e) {
elizaLogger.error("Error fetching available licenses:", e.message);
callback?.({
text: `Error fetching available licenses: ${e.message}`,
});
return false;
}
},
template: getAvailableLicensesTemplate,
validate: async (runtime: IAgentRuntime) => {
return true;
},
examples: [
[
{
user: "assistant",
content: {
text: "Getting available licenses for an IP Asset 0x2265F2b8e47F98b3Bdf7a1937EAc27282954A4Db",
action: "GET_AVAILABLE_LICENSES",
},
},
{
user: "user",
content: {
text: "Get available licenses for an IP Asset 0x2265F2b8e47F98b3Bdf7a1937EAc27282954A4Db",
action: "GET_AVAILABLE_LICENSES",
},
},
],
],
similes: [
"AVAILABLE_LICENSES",
"AVAILABLE_LICENSES_FOR_IP",
"AVAILABLE_LICENSES_FOR_ASSET",
],
};
4 changes: 3 additions & 1 deletion packages/plugin-story/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from "./actions/registerIP";
export * from "./actions/licenseIP";
export * from "./actions/attachTerms";
export * from "./providers/wallet";
export * from "./actions/getAvailableLicenses";
export * from "./providers/pinata";
export * from "./types";

Expand All @@ -10,6 +11,7 @@ import { storyWalletProvider } from "./providers/wallet";
import { storyPinataProvider } from "./providers/pinata";
import { registerIPAction } from "./actions/registerIP";
import { licenseIPAction } from "./actions/licenseIP";
import { getAvailableLicensesAction } from "./actions/getAvailableLicenses";
import { attachTermsAction } from "./actions/attachTerms";

export const storyPlugin: Plugin = {
Expand All @@ -18,7 +20,7 @@ export const storyPlugin: Plugin = {
providers: [storyWalletProvider, storyPinataProvider],
evaluators: [],
services: [],
actions: [registerIPAction, licenseIPAction, attachTermsAction],
actions: [registerIPAction, licenseIPAction, attachTermsAction, getAvailableLicensesAction],
};

export default storyPlugin;
116 changes: 116 additions & 0 deletions packages/plugin-story/src/lib/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import {
IPLicenseTerms,
PILTerms,
QUERY_ORDER_BY,
QUERY_ORDER_DIRECTION,
QueryOptions,
RESOURCE_TYPE,
ResourceType,
Trait,
} from "../types/api";
import { elizaLogger } from "@ai16z/eliza";

import { camelize } from "./utils";
const API_BASE_URL = process.env.STORY_API_BASE_URL;
const API_VERSION = "v2";
export const API_URL = `${API_BASE_URL}/${API_VERSION}`;
export const API_KEY = process.env.STORY_API_KEY || "";

export async function getResource(
resourceName: ResourceType,
resourceId: string,
options?: QueryOptions
) {
try {
const res = await fetch(`${API_URL}/${resourceName}/${resourceId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"x-api-key": API_KEY as string,
"x-chain": "1516",
},
});
if (res.ok) {
return res.json();
}
} catch (error) {
console.error(error);
}
}

export async function listResource(
resourceName: ResourceType,
options?: QueryOptions
) {
try {
const _options = {
pagination: {
limit: 10,
offset: 0,
},
orderBy: QUERY_ORDER_BY.BLOCK_NUMBER,
orderDirection: QUERY_ORDER_DIRECTION.DESC,
...options,
};
elizaLogger.log(`Calling Story API ${resourceName}`);
elizaLogger.log(`STORY_API_KEY: ${API_KEY}`);
elizaLogger.log(`API_URL: ${API_URL}`);
elizaLogger.log(`API_VERSION: ${API_VERSION}`);
elizaLogger.log(`_options: ${JSON.stringify(_options)}`);
const res = await fetch(`${API_URL}/${resourceName}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": API_KEY as string,
"x-chain": "1516",
},
cache: "no-cache",
...(_options && { body: JSON.stringify({ options: _options }) }),
});
if (res.ok) {
elizaLogger.log("Response is ok");
elizaLogger.log(res.ok);
return res.json();
} else {
elizaLogger.log("Response is not ok");
elizaLogger.log(res);
return res;
}
} catch (error) {
elizaLogger.log("List resource Error");
console.error(error);
}
}

export async function fetchLicenseTermsDetails(data: IPLicenseTerms[]) {
const requests = data.map((item) =>
getResource(RESOURCE_TYPE.LICENSE_TERMS, item.licenseTermsId)
);
const results = await Promise.all(requests);

return results
.filter((value) => !!value)
.map((result) => {
return {
...result.data,
licenseTerms: convertLicenseTermObject(
result.data.licenseTerms
),
};
});
}

type LicenseTerms = Partial<PILTerms>;

export function convertLicenseTermObject(licenseTerms: Trait[]): LicenseTerms {
return licenseTerms.reduce((acc, option: Trait): LicenseTerms => {
const key = camelize(option.trait_type) as keyof PILTerms;
acc[key] =
option.value === "true"
? true
: option.value === "false"
? false
: (option.value as any);
return acc as LicenseTerms;
}, {});
}
6 changes: 6 additions & 0 deletions packages/plugin-story/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function camelize(str: string) {
return str.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, function (match, index) {
if (+match === 0) return ""; // or if (/\s+/.test(match)) for white spaces
return index === 0 ? match.toLowerCase() : match.toUpperCase();
});
}
2 changes: 1 addition & 1 deletion packages/plugin-story/src/providers/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class WalletProvider {

this.runtime = runtime;

const account = privateKeyToAccount(privateKey as `0x${string}`);
const account = privateKeyToAccount(privateKey as Address);
this.address = account.address;

const config: StoryConfig = {
Expand Down
18 changes: 18 additions & 0 deletions packages/plugin-story/src/templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,24 @@ Respond with a JSON markdown block containing only the extracted values. A user
\`\`\`
`;

export const getAvailableLicensesTemplate = `Given the recent messages and wallet information below:
{{recentMessages}}
{{walletInfo}}
Extract the following information about the requested IP licensing:
- Field "ipid": The IP Asset that you want to mint a license from
Respond with a JSON markdown block containing only the extracted values:
\`\`\`json
{
"ipid": string | null
}
\`\`\`
`;

export const attachTermsTemplate = `Given the recent messages below:
{{recentMessages}}
Expand Down
Loading

0 comments on commit 550b110

Please sign in to comment.